diff --git a/changelog.d/3448.feature b/changelog.d/3448.feature new file mode 100644 index 00000000000..3f83f1bef5f --- /dev/null +++ b/changelog.d/3448.feature @@ -0,0 +1 @@ +Use UnifiedPush and allows user to have push without FCM. diff --git a/dependencies.gradle b/dependencies.gradle index 7666a3bf9f8..81e3b1b4bc0 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -40,6 +40,7 @@ ext.libs = [ ], jetbrains : [ + 'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$kotlin", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" @@ -82,6 +83,7 @@ ext.libs = [ ], squareup : [ 'moshi' : "com.squareup.moshi:moshi-adapters:$moshi", + 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 8422e05930f..b0bc881d4d6 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -12,6 +12,7 @@ ext.groups = [ 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', + 'com.github.UnifiedPush', ] ], jitsi : [ diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index 78b92cbfa4f..cae094f4547 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -17,7 +17,11 @@ --> + https://matrix.org/_matrix/push/v1/notify + + + https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify im.vector.app.android diff --git a/vector/build.gradle b/vector/build.gradle index 162d9fe81c8..9e92f113333 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -268,6 +268,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } fdroid { @@ -279,6 +280,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } } @@ -334,6 +336,7 @@ dependencies { implementation project(":library:multipicker") implementation 'androidx.multidex:multidex:2.0.1' + implementation libs.jetbrains.kotlinReflect implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -350,6 +353,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0" implementation libs.squareup.moshi + implementation libs.squareup.moshiKt kapt libs.squareup.moshiKotlin // Lifecycle @@ -453,8 +457,10 @@ dependencies { // Analytics implementation 'com.posthog.android:posthog:1.1.2' - // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { + // UnifiedPush + implementation 'com.github.UnifiedPush:android-connector:2.0.0' + // UnifiedPush gplay flavor only + gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index ea9fa023ab8..ca008043c24 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -28,6 +28,19 @@ android:enabled="true" android:exported="false" /> + + + + + + + - + - + + - + + diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt new file mode 100644 index 00000000000..14600ccbb38 --- /dev/null +++ b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.push.fcm + +import android.content.Context +import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver + +class EmbeddedFCMDistributor : EmbeddedDistributorReceiver() { + override fun getEndpoint(context: Context, token: String, instance: String): String { + // Here token is the FCM Token, used by the gateway (sygnal) + return token + } +} diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 52ad4be087d..395a0daf33f 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -95,9 +95,9 @@ object FcmHelper { * it doesn't, display a dialog that allows users to download the APK from * the Google Play Store or enable it in the device's system settings. */ - private fun checkPlayServices(activity: Activity): Boolean { + fun checkPlayServices(context: Context): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity) + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) return resultCode == ConnectionResult.SUCCESS } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index e96c603e608..7c9e8b8c264 100644 --- a/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -16,15 +16,22 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment +import im.vector.app.BuildConfig +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TestAccountSettings +import im.vector.app.features.settings.troubleshoot.TestAvailableUnifiedPushDistributors +import im.vector.app.features.settings.troubleshoot.TestCurrentUnifiedPushDistributor import im.vector.app.features.settings.troubleshoot.TestDeviceSettings +import im.vector.app.features.settings.troubleshoot.TestEndpointAsTokenRegistration import im.vector.app.features.settings.troubleshoot.TestNotification +import im.vector.app.features.settings.troubleshoot.TestPushFromPushGateway import im.vector.app.features.settings.troubleshoot.TestPushRulesSettings import im.vector.app.features.settings.troubleshoot.TestSystemSettings +import im.vector.app.features.settings.troubleshoot.TestUnifiedPushEndpoint +import im.vector.app.features.settings.troubleshoot.TestUnifiedPushGateway import im.vector.app.gplay.features.settings.troubleshoot.TestFirebaseToken import im.vector.app.gplay.features.settings.troubleshoot.TestPlayServices -import im.vector.app.gplay.features.settings.troubleshoot.TestPushFromPushGateway import im.vector.app.gplay.features.settings.troubleshoot.TestTokenRegistration import javax.inject.Inject @@ -32,10 +39,15 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( private val testSystemSettings: TestSystemSettings, private val testAccountSettings: TestAccountSettings, private val testDeviceSettings: TestDeviceSettings, - private val testBingRulesSettings: TestPushRulesSettings, + private val testPushRulesSettings: TestPushRulesSettings, private val testPlayServices: TestPlayServices, private val testFirebaseToken: TestFirebaseToken, private val testTokenRegistration: TestTokenRegistration, + private val testCurrentUnifiedPushDistributor: TestCurrentUnifiedPushDistributor, + private val testUnifiedPushGateway: TestUnifiedPushGateway, + private val testUnifiedPushEndpoint: TestUnifiedPushEndpoint, + private val testAvailableUnifiedPushDistributors: TestAvailableUnifiedPushDistributors, + private val testEndpointAsTokenRegistration: TestEndpointAsTokenRegistration, private val testPushFromPushGateway: TestPushFromPushGateway, private val testNotification: TestNotification ) { @@ -45,10 +57,20 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testSystemSettings) mgr.addTest(testAccountSettings) mgr.addTest(testDeviceSettings) - mgr.addTest(testBingRulesSettings) - mgr.addTest(testPlayServices) - mgr.addTest(testFirebaseToken) - mgr.addTest(testTokenRegistration) + mgr.addTest(testPushRulesSettings) + if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + mgr.addTest(testAvailableUnifiedPushDistributors) + mgr.addTest(testCurrentUnifiedPushDistributor) + } + if (UnifiedPushHelper.isEmbeddedDistributor(fragment.requireContext())) { + mgr.addTest(testPlayServices) + mgr.addTest(testFirebaseToken) + mgr.addTest(testTokenRegistration) + } else { + mgr.addTest(testUnifiedPushGateway) + mgr.addTest(testUnifiedPushEndpoint) + mgr.addTest(testEndpointAsTokenRegistration) + } mgr.addTest(testPushFromPushGateway) mgr.addTest(testNotification) return mgr diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 20b7c4908ae..939c3adf759 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -409,6 +409,17 @@ + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index b1bb4c7d882..7b37de17095 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -16,6 +16,7 @@ package im.vector.app.core.pushers +import android.content.Context import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider @@ -34,13 +35,13 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider ) { - suspend fun testPush(pushKey: String) { + suspend fun testPush(context: Context) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - stringProvider.getString(R.string.pusher_http_url), + UnifiedPushHelper.getPushGateway(context)!!, stringProvider.getString(R.string.pusher_app_id), - pushKey, + UnifiedPushHelper.getEndpointOrToken(context) ?: "", TEST_EVENT_ID ) } @@ -50,19 +51,38 @@ class PushersManager @Inject constructor( return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) } + fun enqueueRegisterPusher( + pushKey: String, + gateway: String + ): UUID { + val currentSession = activeSessionHolder.getActiveSession() + return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey, gateway)) + } + suspend fun registerPusherWithFcmKey(pushKey: String) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) } - private fun createHttpPusher(pushKey: String) = PushersService.HttpPusher( + suspend fun registerPusher( + pushKey: String, + gateway: String + ) { + val currentSession = activeSessionHolder.getActiveSession() + currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey, gateway)) + } + + private fun createHttpPusher( + pushKey: String, + gateway: String = stringProvider.getString(R.string.pusher_http_url) + ) = PushersService.HttpPusher( pushKey, stringProvider.getString(R.string.pusher_app_id), profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), localeProvider.current().language, appNameProvider.getAppName(), activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", - stringProvider.getString(R.string.pusher_http_url), + gateway, append = false, withEventIdOnly = true ) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt new file mode 100644 index 00000000000..c28cd8325eb --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.pushers + +import android.content.Context +import android.content.pm.PackageManager +import androidx.appcompat.app.AlertDialog +import androidx.core.content.edit +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import okhttp3.Request +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import java.net.URL + +object UnifiedPushHelper { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + private val up = UnifiedPush + + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpointOrToken(context: Context): String? { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(context: Context, + endpoint: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(context: Context): String? { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_PUSH_GATEWAY, null) + } + + /** + * Store Push Gateway to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param gateway the push gateway to store + */ + private fun storePushGateway(context: Context, + gateway: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } + + fun register(context: Context, onDoneRunnable: Runnable? = null) { + gRegister(context, + onDoneRunnable = onDoneRunnable) + } + + fun reRegister(context: Context, + pushersManager: PushersManager, + vectorPreferences: VectorPreferences, + onDoneRunnable: Runnable? = null) { + gRegister(context, + force = true, + pushersManager = pushersManager, + vectorPreferences = vectorPreferences, + onDoneRunnable = onDoneRunnable + ) + } + + private fun gRegister(context: Context, + force: Boolean = false, + pushersManager: PushersManager? = null, + vectorPreferences: VectorPreferences? = null, + onDoneRunnable: Runnable? = null) { + if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + up.saveDistributor(context, context.packageName) + up.registerApp(context) + onDoneRunnable?.run() + return + } + if (force) { + // Un-register first + unregister(context, pushersManager, vectorPreferences) + } + if (up.getDistributor(context).isNotEmpty()) { + up.registerApp(context) + onDoneRunnable?.run() + return + } + + // By default, use internal solution (fcm/background sync) + up.saveDistributor(context, context.packageName) + val distributors = up.getDistributors(context).toMutableList() + + val internalDistributorName = if (FcmHelper.isPushSupported()) { + context.getString(R.string.unifiedpush_distributor_fcm_fallback) + } else { + context.getString(R.string.unifiedpush_distributor_background_sync) + } + + if (distributors.size == 1 && + !force) { + up.saveDistributor(context, distributors.first()) + up.registerApp(context) + onDoneRunnable?.run() + } else { + val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(context) + builder.setTitle(context.getString(R.string.unifiedpush_getdistributors_dialog_title)) + + val distributorsArray = distributors.toTypedArray() + val distributorsNameArray = distributorsArray.map { + if (it == context.packageName) { + internalDistributorName + } else { + try { + val ai = context.packageManager.getApplicationInfo(it, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + } + }.toTypedArray() + builder.setItems(distributorsNameArray) { _, which -> + val distributor = distributorsArray[which] + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) + onDoneRunnable?.run() + } + builder.setOnDismissListener { + onDoneRunnable?.run() + } + builder.setOnCancelListener { + onDoneRunnable?.run() + } + val dialog: AlertDialog = builder.create() + dialog.show() + } + } + + fun unregister( + context: Context, + pushersManager: PushersManager? = null, + vectorPreferences: VectorPreferences? = null + ) { + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences?.setFdroidSyncBackgroundMode(mode) + runBlocking { + try { + pushersManager?.unregisterPusher(getEndpointOrToken(context) ?: "") + } catch (e: Exception) { + Timber.d("Probably unregistering a non existant pusher") + } + } + storeUpEndpoint(context, null) + storePushGateway(context, null) + up.unregisterApp(context) + } + + @JsonClass(generateAdapter = true) + internal data class DiscoveryResponse( + val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() + ) + + @JsonClass(generateAdapter = true) + internal data class DiscoveryUnifiedPush( + val gateway: String = "" + ) + + fun storeCustomOrDefaultGateway( + context: Context, + endpoint: String, + onDoneRunnable: Runnable? = null) { + // if we use the embedded distributor, + // register app_id type upfcm on sygnal + // the pushkey if FCM key + if (up.getDistributor(context) == context.packageName) { + context.getString(R.string.pusher_http_url).let { + storePushGateway(context, it) + onDoneRunnable?.run() + return + } + } + // else, unifiedpush, and pushkey is an endpoint + val gateway = context.getString(R.string.default_push_gateway_http_url) + val parsed = URL(endpoint) + val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + val thread = CoroutineScope(SupervisorJob()).launch { + try { + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val client = OkHttpClient() + val request = Request.Builder() + .url(custom) + .build() + val sResponse = client.newCall(request).execute() + .body?.string() ?: "" + moshi.adapter(DiscoveryResponse::class.java) + .fromJson(sResponse)?.let { response -> + if (response.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + storePushGateway(context, custom) + onDoneRunnable?.run() + return@launch + } + } + } catch (e: Exception) { + Timber.d("Cannot try custom gateway: $e") + } + storePushGateway(context, gateway) + onDoneRunnable?.run() + return@launch + } + thread.start() + } + + fun getExternalDistributors(context: Context): List { + val distributors = up.getDistributors(context).toMutableList() + distributors.remove(context.packageName) + return distributors + } + + fun getCurrentDistributorName(context: Context): String { + if (isEmbeddedDistributor(context)) { + return context.getString(R.string.unifiedpush_distributor_fcm_fallback) + } + if (isBackgroundSync(context)) { + return context.getString(R.string.unifiedpush_distributor_background_sync) + } + val distributor = up.getDistributor(context) + return try { + val ai = context.packageManager.getApplicationInfo(distributor, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + distributor + } as String + } + + fun isEmbeddedDistributor(context: Context): Boolean { + return (up.getDistributor(context) == context.packageName && + FcmHelper.isPushSupported()) + } + + fun isBackgroundSync(context: Context): Boolean { + return (up.getDistributor(context) == context.packageName && + !FcmHelper.isPushSupported()) + } + + fun getPrivacyFriendlyUpEndpoint(context: Context): String? { + val endpoint = getEndpointOrToken(context) + if (endpoint.isNullOrEmpty()) return endpoint + if (isEmbeddedDistributor(context)) { + return endpoint + } + return try { + val parsed = URL(endpoint) + "${parsed.protocol}://${parsed.host}/***" + } catch (e: Exception) { + Timber.e("Error parsing unifiedpush endpoint: $e") + null + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt old mode 100755 new mode 100644 similarity index 58% rename from vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt rename to vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 8003b63ba0e..e1253820532 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -1,14 +1,11 @@ /* - * Copyright 2019 New Vector Ltd - * + * Copyright (c) 2022 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,28 +14,32 @@ * limitations under the License. */ -package im.vector.app.gplay.push.fcm +package im.vector.app.core.pushers +import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper +import android.widget.Toast import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector -import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -47,24 +48,43 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject +@JsonClass(generateAdapter = true) +data class UnifiedPushMessage( + val notification: Notification = Notification() +) + +@JsonClass(generateAdapter = true) +data class Notification( + @Json(name = "event_id") val eventId: String = "", + @Json(name = "room_id") val roomId: String = "", + var unread: Int = 0, + val counts: Counts = Counts() +) + +@JsonClass(generateAdapter = true) +data class Counts( + val unread: Int = 0 +) + private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) /** - * Class extending FirebaseMessagingService. + * Hilt injection happen at super.onReceive(). */ @AndroidEntryPoint -class VectorFirebaseMessagingService : FirebaseMessagingService() { - +class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notifiableEventResolver: NotifiableEventResolver - @Inject lateinit var pusherManager: PushersManager + @Inject lateinit var pushersManager: PushersManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorDataStore: VectorDataStore @Inject lateinit var wifiDetector: WifiDetector + @Inject lateinit var guardServiceStarter: GuardServiceStarter private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -77,21 +97,38 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * Called when message is received. * * @param message the message + * @param instance connection, for multi-account */ - override fun onMessageReceived(message: RemoteMessage) { + override fun onMessage(context: Context, message: ByteArray, instance: String) { + Timber.tag(loggerTag.value).d("## onMessage() received") + val sMessage = String(message) if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessage() %s", sMessage) } - Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) runBlocking { vectorDataStore.incrementPushCounter() } + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + lateinit var notification: Notification + + if (UnifiedPushHelper.isEmbeddedDistributor(context)) { + notification = moshi.adapter(Notification::class.java) + .fromJson(sMessage) ?: return + } else { + val data = moshi.adapter(UnifiedPushMessage::class.java) + .fromJson(sMessage) ?: return + notification = data.notification + notification.unread = notification.counts.unread + } + // Diagnostic Push - if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { + if (notification.eventId == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) - LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) return } @@ -105,7 +142,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // we are in foreground, let the sync do the things? Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { - onMessageReceivedInternal(message.data) + onMessageReceivedInternal(context, notification) } } } @@ -116,59 +153,75 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * when the InstanceID token is initially generated, so this is where * you retrieve the token. */ - override fun onNewToken(refreshedToken: String) { - Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") - FcmHelper.storeFcmToken(this, refreshedToken) + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken) + // If the endpoint has changed + // or the gateway has changed + if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint) { + UnifiedPushHelper.storeUpEndpoint(context, endpoint) + UnifiedPushHelper.storeCustomOrDefaultGateway(context, endpoint) { + UnifiedPushHelper.getPushGateway(context)?.let { + pushersManager.enqueueRegisterPusher(endpoint, it) + } + } + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") + } } + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.stop() } - /** - * Called when the FCM server deletes pending messages. This may be due to: - * - Too many messages stored on the FCM server. - * This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline. - * - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks) - * sent a message to the app on that device. - * - * It is recommended that the app do a full sync with the app server after receiving this call. - */ - override fun onDeletedMessages() { - Timber.tag(loggerTag.value).v("## onDeletedMessages()") + override fun onRegistrationFailed(context: Context, instance: String) { + Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() + } + + override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences.setFdroidSyncBackgroundMode(mode) + guardServiceStarter.start() + runBlocking { + try { + pushersManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") + } catch (e: Exception) { + Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") + } + } } /** * Internal receive method * - * @param data Data map containing message data as key/value pairs. - * For Set of keys use data.keySet(). + * @param notification Notification containing message data. */ - private fun onMessageReceivedInternal(data: Map) { + private fun onMessageReceivedInternal(context: Context, notification: Notification) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification") } else { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } // update the badge counter - val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0 - BadgeProxy.updateBadgeCount(applicationContext, unreadCount) + BadgeProxy.updateBadgeCount(context, notification.unread) val session = activeSessionHolder.getSafeActiveSession() if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { - val eventId = data["event_id"] - val roomId = data["room_id"] - - if (isEventAlreadyKnown(eventId, roomId)) { + if (isEventAlreadyKnown(notification.eventId, notification.roomId)) { Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, roomId, eventId) + getEventFastLane(session, notification.roomId, notification.eventId) Timber.tag(loggerTag.value).d("Requesting background sync") session.requireBackgroundSync() diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index fe12bf1ec7f..4b96f5e12da 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.CallService import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CallEnded @@ -32,7 +33,6 @@ import im.vector.app.features.call.lookup.CallUserMapper import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.vectorCallService import im.vector.app.features.session.coroutineScope -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.extensions.orFalse @@ -270,7 +270,7 @@ class WebRtcCallManager @Inject constructor( audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { currentSession?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing @@ -375,7 +375,7 @@ class WebRtcCallManager @Inject constructor( // and thus won't be able to received events. For example if the call is // accepted on an other session this device will continue ringing if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { // only for push version as fdroid version is already doing it? currentSession?.startAutomaticBackgroundSync(30, 0) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index d0ae7581beb..fa6aa1b4fc4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -44,6 +44,7 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs @@ -181,7 +182,15 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) + UnifiedPushHelper.register(this) { + if (UnifiedPushHelper.isEmbeddedDistributor(this)) { + FcmHelper.ensureFcmTokenIsRetrieved( + this, + pushManager, + vectorPreferences.areNotificationEnabledForDevice() + ) + } + } sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 195072465bc..1d4aa4eb97a 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -136,6 +136,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" + // notification method + const val SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY = "SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY" + // Calls const val SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY = "SETTINGS_CALL_PREVENT_ACCIDENTAL_CALL_KEY" const val SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index d9cd5b3461a..88bdcd413e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.registerStartForActivityResult @@ -38,6 +39,7 @@ import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.utils.combineLatest import im.vector.app.core.utils.isIgnoringBatteryOptimizations @@ -49,7 +51,6 @@ import im.vector.app.features.settings.BackgroundSyncModeChooserDialog import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -62,7 +63,7 @@ import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml class VectorSettingsNotificationPreferenceFragment @Inject constructor( - private val pushManager: PushersManager, + private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, private val guardServiceStarter: GuardServiceStarter @@ -98,14 +99,14 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.registerPusherWithFcmKey(it) - } + UnifiedPushHelper.register(requireContext()) } else { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.unregisterPusher(it) - session.pushersService().refreshPushers() - } + UnifiedPushHelper.unregister( + requireContext(), + pushersManager, + vectorPreferences + ) + session.pushersService().refreshPushers() } } } @@ -148,6 +149,24 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + findPreference(VectorPreferences.SETTINGS_UNIFIED_PUSH_RE_REGISTER_KEY)?.let { + if (BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + UnifiedPushHelper.reRegister( + requireContext(), + pushersManager, + vectorPreferences + ) { + session.pushersService().refreshPushers() + refreshBackgroundSyncPrefs() + } + true + } + } else { + it.isVisible = false + } + } + bindEmailNotifications() refreshBackgroundSyncPrefs() @@ -182,9 +201,9 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( pref.isChecked = isEnabled pref.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - pushManager.registerEmailForPush(emailPid.email) + pushersManager.registerEmailForPush(emailPid.email) } else { - pushManager.unregisterEmailPusher(emailPid.email) + pushersManager.unregisterEmailPusher(emailPid.email) } } category.addPreference(pref) @@ -222,7 +241,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } findPreference(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { - it.isVisible = !FcmHelper.isPushSupported() + it.isVisible = UnifiedPushHelper.isBackgroundSync(requireContext()) } val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled() @@ -331,7 +350,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private fun refreshPref() { // This pref may have change from troubleshoot pref fragment - if (!FcmHelper.isPushSupported()) { + if (UnifiedPushHelper.isBackgroundSync(requireContext())) { findPreference(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) ?.isChecked = vectorPreferences.autoStartOnBoot() } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt new file mode 100644 index 00000000000..9e25b27d0f3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import im.vector.app.push.fcm.FcmHelper +import javax.inject.Inject + +class TestAvailableUnifiedPushDistributors @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val distributors = UnifiedPushHelper.getExternalDistributors(context) + if (distributors.isEmpty()) { + description = if (FcmHelper.isPushSupported()) { + stringProvider.getString(R.string.settings_troubleshoot_test_distributors_gplay) + } else { + stringProvider.getString(R.string.settings_troubleshoot_test_distributors_fdroid) + } + status = TestStatus.SUCCESS + } else { + description = stringProvider.getString(R.string.settings_troubleshoot_test_distributors_many, + distributors.size + 1) + status = TestStatus.SUCCESS + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt new file mode 100644 index 00000000000..5eda48ffdc2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestCurrentUnifiedPushDistributor @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_distributor_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_distributor, + UnifiedPushHelper.getCurrentDistributorName(context)) + status = TestStatus.SUCCESS + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt new file mode 100644 index 00000000000..558b0e4fd93 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Observer +import androidx.work.WorkInfo +import androidx.work.WorkManager +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.pushers.PusherState +import javax.inject.Inject + +class TestEndpointAsTokenRegistration @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder) : + TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + // Check if we have a registered pusher for this token + val endpoint = UnifiedPushHelper.getEndpointOrToken(context) ?: run { + status = TestStatus.FAILED + return + } + val session = activeSessionHolder.getSafeActiveSession() ?: run { + status = TestStatus.FAILED + return + } + val pushers = session.pushersService().getPushers().filter { + it.pushKey == endpoint && it.state == PusherState.REGISTERED + } + if (pushers.isEmpty()) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_failed, + stringProvider.getString(R.string.sas_error_unknown)) + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { + override fun doFix() { + UnifiedPushHelper.reRegister( + context, + pushersManager, + vectorPreferences + ) + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint) + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> + if (workInfo != null) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + manager?.retry(activityResultLauncher) + } else if (workInfo.state == WorkInfo.State.FAILED) { + manager?.retry(activityResultLauncher) + } + } + }) + } + } + + status = TestStatus.FAILED + } else { + description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success) + status = TestStatus.SUCCESS + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt similarity index 91% rename from vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt rename to vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt index f485de760a0..da26200df24 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.app.gplay.features.settings.troubleshoot +package im.vector.app.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher @@ -24,8 +24,6 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.troubleshoot.TroubleshootTest -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -48,12 +46,8 @@ class TestPushFromPushGateway @Inject constructor(private val context: FragmentA override fun perform(activityResultLauncher: ActivityResultLauncher) { pushReceived = false - val fcmToken = FcmHelper.getFcmToken(context) ?: run { - status = TestStatus.FAILED - return - } action = activeSessionHolder.getActiveSession().coroutineScope.launch { - val result = runCatching { pushersManager.testPush(fcmToken) } + val result = runCatching { pushersManager.testPush(context) } withContext(Dispatchers.Main) { status = result diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt new file mode 100644 index 00000000000..ab4e3dd7c2b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestUnifiedPushEndpoint @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_endpoint_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val endpoint = UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context) + endpoint?.let { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_success, + UnifiedPushHelper.getPrivacyFriendlyUpEndpoint(context)) + status = TestStatus.SUCCESS + } ?: run { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_failed) + status = TestStatus.FAILED + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt new file mode 100644 index 00000000000..75e8962c84e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.troubleshoot + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentActivity +import im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestUnifiedPushGateway @Inject constructor(private val context: FragmentActivity, + private val stringProvider: StringProvider) : + TroubleshootTest(R.string.settings_troubleshoot_test_current_gateway_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_gateway, + UnifiedPushHelper.getPushGateway(context)) + status = TestStatus.SUCCESS + } +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8607e8e0bef..cccc48e9496 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -849,6 +849,10 @@ FCM token successfully registered to homeserver. Failed to register FCM token to homeserver:\n%1$s + Endpoint Registration + Endpoint successfully registered to homeserver. + Failed to register endpoint token to homeserver:\n%1$s + Test Push The application is waiting for the PUSH The application is receiving PUSH @@ -1657,6 +1661,8 @@ Register token + Reset notification method + Make a suggestion Please write your suggestion below. Describe your suggestion here @@ -3038,4 +3044,20 @@ ${app_name} Screen Sharing Screen sharing is in progress + + Choose how to receive notifications + Google Services + Background synchronization + Notification method + Available methods + No other method than Google Play Service found. + No other method than background synchronization found. + Found %d methods. + Method + Currently using %s. + Endpoint + Current endpoint: %s + Cannot find the endpoint. + Gateway + Current gateway: %s diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 66ac93a4f93..c8434b69200 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -77,6 +77,11 @@ android:summary="@string/settings_system_preferences_summary" android:title="@string/settings_call_notifications_preferences" /> + +