diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 85290e72df..c29bca95f2 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -40,6 +40,7 @@ sygnal threepid uisi + unifiedpush unpublish unwedging vctr diff --git a/changelog.d/3448.feature b/changelog.d/3448.feature new file mode 100644 index 0000000000..3f83f1bef5 --- /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 604174fe57..962f07f21f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -89,6 +89,7 @@ ext.libs = [ ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$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 59cefe7e89..e6817f2b23 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -9,6 +9,7 @@ ext.groups = [ 'com.github.jetradarmobile', 'com.github.MatrixFrog', 'com.github.tapadoo', + 'com.github.UnifiedPush', 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', diff --git a/docs/unifiedpush.md b/docs/unifiedpush.md new file mode 100644 index 0000000000..2851644e66 --- /dev/null +++ b/docs/unifiedpush.md @@ -0,0 +1,58 @@ +# UnifiedPush + + + +* [Introduction](#introduction) +* [Configuration in Element-Android and their forks](#configuration-in-element-android-and-their-forks) + * [Enabling and disabling the feature](#enabling-and-disabling-the-feature) + * [Override the configuration at runtime](#override-the-configuration-at-runtime) + * [Enabling the feature](#enabling-the-feature) + * [Disabling the feature](#disabling-the-feature) + * [Useful links](#useful-links) + + + +## Introduction + +The recently started UnifiedPush project is an Android protocol and library for apps to be able to receive distributor-agnostic push notifications. + +The *F-Droid* and *Gplay* flavors of Element Android support UnifiedPush, so the user can use any distributor installed on their devices. This would make it possible to have push notifications without depending on Google services or libraries. Currently, the main distributors are [ntfy](https://ntfy.sh) which does not require any setup (like manual registration) to use the public server and [NextPush](https://github.com/UP-NextPush/android), available as a nextcloud application. + +The *Gplay* variant uses a UnifiedPush library which basically embed a FCM distributor built into the application (so a user doesn't need to do anything other than install the app to get FCM notifications). This variant uses Google Services to receive notifications if the user has not installed any distributor. + +The *F-Droid* variant does not use this library to avoid any proprietary blob. It will use a polling service if the user has not installed any distributor. + +In all cases, if there are other distributors available, the user will have to opt-in to one of them in the preferences. + +## Configuration in Element-Android and their forks + +### Enabling and disabling the feature + +Allowing the user to use an alternative distributor can be changed in [Config](../vector-config/src/main/java/im/vector/app/config/Config.kt). The flag is named `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS`. Default value is `true`. + +#### Override the configuration at runtime + +On debug version, it is possible to override this configuration at runtime, using the `Feature` screen. The Feature is named `Allow external UnifiedPush distributors`. + +#### Enabling the feature + +This is the default behavior of Element Android. + +If `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS` is set to true, it allows any available external UnifiedPush distributor to be chosen by the user. +- For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; +- For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. +- On the UI, the setting to choose an alternative distributor will be visible to the user, and some tests in the notification troubleshoot screen will shown. +- For F-Droid, if the user has chosen a distributor, the settings to configure the background polling will be hidden. + +#### Disabling the feature + +If `ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS` is set to false, it prevents the usage of external UnifiedPush distributors. +- For Gplay variant it means that only FCM will be used; +- For F-Droid variant, it means that only background polling will be used. +- On the UI, the setting to choose an alternative distributor will be hidden to the user, and some tests in the notification troubleshoot screen will be hidden. + +### Useful links + +- UnifiedPush official website: [https://unifiedpush.org/](https://unifiedpush.org/) +- List of available distributors can be retrieved here: [https://unifiedpush.org/users/distributors/](https://unifiedpush.org/users/distributors/) +- UnifiedPush project discussion can occurs here: [#unifiedpush:matrix.org](https://matrix.to/#/#unifiedpush:matrix.org) diff --git a/vector-config/build.gradle b/vector-config/build.gradle index 95b6a6215d..658452bbdd 100644 --- a/vector-config/build.gradle +++ b/vector-config/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'kotlin-android' } android { @@ -13,4 +14,8 @@ android { sourceCompatibility versions.sourceCompat targetCompatibility versions.targetCompat } + + kotlinOptions { + jvmTarget = "11" + } } diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt new file mode 100644 index 0000000000..7577e6dba5 --- /dev/null +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -0,0 +1,39 @@ +/* + * 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.config + +/** + * Set of flags to configure the application. + */ +object Config { + /** + * Flag to allow external UnifiedPush distributors to be chosen by the user. + * + * Set to true to allow any available external UnifiedPush distributor to be chosen by the user. + * - For Gplay variant it means that FCM will be used by default, but user can choose another UnifiedPush distributor; + * - For F-Droid variant, it means that background polling will be used by default, but user can choose another UnifiedPush distributor. + * + * Set to false to prevent usage of external UnifiedPush distributors. + * - For Gplay variant it means that only FCM will be used; + * - For F-Droid variant, it means that only background polling will be available to the user. + * + * *Note*: When the app is already installed on users' phone: + * - Changing the value from `false` to `true` will let the user be able to select an external UnifiedPush distributor; + * - Changing the value from `true` to `false` will force the app to return to the background sync / Firebase Push. + */ + const val ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS = true +} diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index 78b92cbfa4..cae094f454 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 46659f66a8..193cd1b080 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -366,6 +366,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi + implementation libs.squareup.moshiKt kapt libs.squareup.moshiKotlin // Lifecycle @@ -461,8 +462,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/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt index 433a70b5e3..5858e78a2a 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsNotificationsRobot.kt @@ -17,6 +17,7 @@ package im.vector.app.ui.robot.settings import androidx.test.espresso.Espresso.pressBack +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.espresso.tools.clickOnPreference @@ -41,7 +42,18 @@ class SettingsNotificationsRobot { clickOn(R.string.settings_call_notifications_preferences) pressBack() */ + // Email notification. No Emails are configured so we show go to the screen to add email + clickOnPreference(R.string.settings_notification_emails_no_emails) + assertDisplayed(R.string.settings_emails_and_phone_numbers_title) + pressBack() + + // Display the notification method change dialog + clickOnPreference(R.string.settings_notification_method) + pressBack() + clickOnPreference(R.string.settings_notification_troubleshoot) + // Give time for the tests to perform + Thread.sleep(12_000) pressBack() } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index aa4df5e308..248d9d232b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -65,6 +65,11 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.onboardingCombinedLogin, factory = VectorFeatures::isOnboardingCombinedLoginEnabled ), + createBooleanFeature( + label = "Allow external UnifiedPush distributors", + key = DebugFeatureKeys.allowExternalUnifiedPushDistributors, + factory = VectorFeatures::allowExternalUnifiedPushDistributors + ), ) ) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index f36b1a804a..919cc6635e 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -60,6 +60,9 @@ class DebugVectorFeatures( override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin) ?: vectorFeatures.isOnboardingCombinedLoginEnabled() + override fun allowExternalUnifiedPushDistributors(): Boolean = read(DebugFeatureKeys.allowExternalUnifiedPushDistributors) + ?: vectorFeatures.allowExternalUnifiedPushDistributors() + override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing) ?: vectorFeatures.isScreenSharingEnabled() @@ -117,6 +120,7 @@ object DebugFeatureKeys { val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize") val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register") val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login") + val allowExternalUnifiedPushDistributors = booleanPreferencesKey("allow-external-unified-push-distributors") val liveLocationSharing = booleanPreferencesKey("live-location-sharing") val screenSharing = booleanPreferencesKey("screen-sharing") } diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index ea9fa023ab..f9adc521c9 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -28,6 +28,20 @@ android:enabled="true" android:exported="false" /> + + + + + + + - \ No newline at end of file + diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt index e2ac4f8822..eaa3d57d42 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -23,14 +23,14 @@ import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import timber.log.Timber +import javax.inject.Inject -object BackgroundSyncStarter { - fun start( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { +class BackgroundSyncStarter @Inject constructor( + private val context: Context, + private val vectorPreferences: VectorPreferences, + private val clock: Clock +) { + fun start(activeSessionHolder: ActiveSessionHolder) { if (vectorPreferences.areNotificationEnabledForDevice()) { val activeSession = activeSessionHolder.getSafeActiveSession() ?: return when (vectorPreferences.getFdroidSyncBackgroundMode()) { diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt new file mode 100644 index 0000000000..3feee8c63b --- /dev/null +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/KeepInternalDistributor.kt @@ -0,0 +1,30 @@ +/* + * 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.fdroid.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +/** + * UnifiedPush lib tracks an action to check installed and uninstalled distributors. + * We declare it to keep the background sync as an internal unifiedpush distributor. + * This class is used to declare this action. + */ +class KeepInternalDistributor : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) {} +} diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index aacd7723f5..f22aafbeb4 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -20,20 +20,20 @@ package im.vector.app.fdroid.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import im.vector.app.core.extensions.singletonEntryPoint +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.fdroid.BackgroundSyncStarter import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var backgroundSyncStarter: BackgroundSyncStarter + override fun onReceive(context: Context, intent: Intent) { Timber.v("## onReceive() ${intent.action}") - val singletonEntryPoint = context.singletonEntryPoint() - BackgroundSyncStarter.start( - context, - singletonEntryPoint.vectorPreferences(), - singletonEntryPoint.activeSessionHolder(), - singletonEntryPoint.clock() - ) + backgroundSyncStarter.start(activeSessionHolder) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7533eae856..24ff00a353 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,34 +21,35 @@ import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.time.Clock import im.vector.app.fdroid.BackgroundSyncStarter import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver -import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject /** * This class has an alter ego in the gplay variant. */ -object FcmHelper { +class FcmHelper @Inject constructor( + private val context: Context, + private val backgroundSyncStarter: BackgroundSyncStarter, +) { - fun isPushSupported(): Boolean = false + fun isFirebaseAvailable(): Boolean = false /** * Retrieves the FCM registration token. * * @return the FCM token or null if not received from FCM */ - fun getFcmToken(context: Context): String? { + fun getFcmToken(): String? { return null } /** * Store FCM token to the SharedPrefs * - * @param context android context * @param token the token to store */ - fun storeFcmToken(context: Context, token: String?) { + fun storeFcmToken(token: String?) { // No op } @@ -61,18 +62,13 @@ object FcmHelper { // No op } - fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { // try to stop all regardless of background mode activeSessionHolder.getSafeActiveSession()?.syncService()?.stopAnyBackgroundSync() AlarmSyncBroadcastReceiver.cancelAlarm(context) } - fun onEnterBackground( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { - BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder, clock) + fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { + backgroundSyncStarter.start(activeSessionHolder) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index 6236aad65c..5873b4308f 100644 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -16,26 +16,42 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot import im.vector.app.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions import im.vector.app.fdroid.features.settings.troubleshoot.TestBatteryOptimization +import im.vector.app.features.VectorFeatures 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 javax.inject.Inject class NotificationTroubleshootTestManagerFactory @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, private val testSystemSettings: TestSystemSettings, private val testAccountSettings: TestAccountSettings, private val testDeviceSettings: TestDeviceSettings, private val testPushRulesSettings: TestPushRulesSettings, + 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 testAutoStartBoot: TestAutoStartBoot, private val testBackgroundRestrictions: TestBackgroundRestrictions, private val testBatteryOptimization: TestBatteryOptimization, - private val testNotification: TestNotification + private val testNotification: TestNotification, + private val vectorFeatures: VectorFeatures, ) { fun create(fragment: Fragment): NotificationTroubleshootTestManager { @@ -44,9 +60,20 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor( mgr.addTest(testAccountSettings) mgr.addTest(testDeviceSettings) mgr.addTest(testPushRulesSettings) - mgr.addTest(testAutoStartBoot) - mgr.addTest(testBackgroundRestrictions) - mgr.addTest(testBatteryOptimization) + if (vectorFeatures.allowExternalUnifiedPushDistributors()) { + mgr.addTest(testAvailableUnifiedPushDistributors) + mgr.addTest(testCurrentUnifiedPushDistributor) + } + if (unifiedPushHelper.isBackgroundSync()) { + mgr.addTest(testAutoStartBoot) + mgr.addTest(testBackgroundRestrictions) + mgr.addTest(testBatteryOptimization) + } else { + mgr.addTest(testUnifiedPushGateway) + mgr.addTest(testUnifiedPushEndpoint) + mgr.addTest(testEndpointAsTokenRegistration) + mgr.addTest(testPushFromPushGateway) + } mgr.addTest(testNotification) return mgr } diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index f541eebd83..c0c0c4ef0f 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -9,13 +9,17 @@ android:name="firebase_analytics_collection_deactivated" android:value="true" /> - + - + + - + + diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 5a9dc90ec4..e7e3157f6b 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -32,7 +32,8 @@ import javax.inject.Inject */ class TestFirebaseToken @Inject constructor( private val context: FragmentActivity, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val fcmHelper: FcmHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { @@ -68,7 +69,7 @@ class TestFirebaseToken @Inject constructor( description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") // Ensure it is well store in our local storage - FcmHelper.storeFcmToken(context, token) + fcmHelper.storeFcmToken(token) } status = TestStatus.SUCCESS } diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index a6220c2018..8c21404d20 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -37,13 +37,14 @@ class TestTokenRegistration @Inject constructor( private val context: FragmentActivity, private val stringProvider: StringProvider, private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder + private val activeSessionHolder: ActiveSessionHolder, + private val fcmHelper: FcmHelper, ) : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { // Check if we have a registered pusher for this token - val fcmToken = FcmHelper.getFcmToken(context) ?: run { + val fcmToken = fcmHelper.getFcmToken() ?: run { status = TestStatus.FAILED return } 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 0000000000..14600ccbb3 --- /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 ac2d063700..a4eb9efc73 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 @@ -26,40 +26,41 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.time.Clock -import im.vector.app.features.settings.VectorPreferences import timber.log.Timber +import javax.inject.Inject /** * This class store the FCM token in SharedPrefs and ensure this token is retrieved. * It has an alter ego in the fdroid variant. */ -object FcmHelper { - private val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" +class FcmHelper @Inject constructor( + context: Context, +) { + companion object { + private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + } - fun isPushSupported(): Boolean = true + private val sharedPrefs = DefaultSharedPreferences.getInstance(context) + + fun isFirebaseAvailable(): Boolean = true /** * Retrieves the FCM registration token. * * @return the FCM token or null if not received from FCM */ - fun getFcmToken(context: Context): String? { - return DefaultSharedPreferences.getInstance(context).getString(PREFS_KEY_FCM_TOKEN, null) + fun getFcmToken(): String? { + return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) } /** * Store FCM token to the SharedPrefs * TODO Store in realm * - * @param context android context * @param token the token to store */ - fun storeFcmToken( - context: Context, - token: String? - ) { - DefaultSharedPreferences.getInstance(context).edit { + fun storeFcmToken(token: String?) { + sharedPrefs.edit { putString(PREFS_KEY_FCM_TOKEN, token) } } @@ -76,7 +77,7 @@ object FcmHelper { try { FirebaseMessaging.getInstance().token .addOnSuccessListener { token -> - storeFcmToken(activity, token) + storeFcmToken(token) if (registerPusher) { pushersManager.enqueueRegisterPusherWithFcmKey(token) } @@ -98,24 +99,19 @@ 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 { + private fun checkPlayServices(context: Context): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity) + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) return resultCode == ConnectionResult.SUCCESS } @Suppress("UNUSED_PARAMETER") - fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { // No op } @Suppress("UNUSED_PARAMETER") - fun onEnterBackground( - context: Context, - vectorPreferences: VectorPreferences, - activeSessionHolder: ActiveSessionHolder, - clock: Clock - ) { + fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { // No op } } 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 e96c603e60..b3425c778b 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,28 +16,42 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.VectorFeatures 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 class NotificationTroubleshootTestManagerFactory @Inject constructor( + private val unifiedPushHelper: UnifiedPushHelper, 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 + private val testNotification: TestNotification, + private val vectorFeatures: VectorFeatures, ) { fun create(fragment: Fragment): NotificationTroubleshootTestManager { @@ -45,10 +59,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 (vectorFeatures.allowExternalUnifiedPushDistributors()) { + mgr.addTest(testAvailableUnifiedPushDistributors) + mgr.addTest(testCurrentUnifiedPushDistributor) + } + if (unifiedPushHelper.isEmbeddedDistributor()) { + 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 89c600b052..1e7a40cef2 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -304,7 +304,8 @@ android:supportsPictureInPicture="true" /> - @@ -410,6 +411,20 @@ + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 7db0f99f5f..e888a257ef 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -43,7 +43,6 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.time.Clock import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration @@ -86,7 +85,6 @@ class VectorApplication : @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var clock: Clock @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider @@ -100,6 +98,7 @@ class VectorApplication : @Inject lateinit var vectorFileLogger: VectorFileLogger @Inject lateinit var vectorAnalytics: VectorAnalytics @Inject lateinit var matrix: Matrix + @Inject lateinit var fcmHelper: FcmHelper // font thread handler private var fontThreadHandler: Handler? = null @@ -174,7 +173,7 @@ class VectorApplication : ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { Timber.i("App entered foreground") - FcmHelper.onEnterForeground(appContext, activeSessionHolder) + fcmHelper.onEnterForeground(activeSessionHolder) activeSessionHolder.getSafeActiveSession()?.also { it.syncService().stopAnyBackgroundSync() } @@ -182,7 +181,7 @@ class VectorApplication : override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") - FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder, clock) + fcmHelper.onEnterBackground(activeSessionHolder) } }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index f0c956365f..ef7f0896b8 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -18,6 +18,7 @@ package im.vector.app.core.di import arrow.core.Option import im.vector.app.ActiveSessionDataSource +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler @@ -39,6 +40,7 @@ class ActiveSessionHolder @Inject constructor( private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener, private val imageManager: ImageManager, + private val unifiedPushHelper: UnifiedPushHelper, private val guardServiceStarter: GuardServiceStarter ) { @@ -58,7 +60,7 @@ class ActiveSessionHolder @Inject constructor( guardServiceStarter.start() } - fun clearActiveSession() { + suspend fun clearActiveSession() { // Do some cleanup first getSafeActiveSession()?.let { Timber.w("clearActiveSession of ${it.myUserId}") @@ -72,6 +74,8 @@ class ActiveSessionHolder @Inject constructor( keyRequestHandler.stop() incomingVerificationRequestHandler.stop() pushRuleTriggerListener.stop() + // No need to unregister the pusher, the sign out will (should?) do it server side. + unifiedPushHelper.unregister(pushersManager = null) guardServiceStarter.stop() } diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt b/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt new file mode 100644 index 0000000000..6f141e3736 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/PushParser.kt @@ -0,0 +1,51 @@ +/* + * 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 im.vector.app.core.pushers.model.PushData +import im.vector.app.core.pushers.model.PushDataFcm +import im.vector.app.core.pushers.model.PushDataUnifiedPush +import im.vector.app.core.pushers.model.toPushData +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.MatrixJsonParser +import javax.inject.Inject + +class PushParser @Inject constructor() { + /** + * Parse the received data from Push. Json format are different depending on the source. + * + * Notifications received by FCM are formatted by the matrix gateway [1]. The data send to FCM is the content + * of the "notification" attribute of the json sent to the gateway [2][3]. + * On the other side, with UnifiedPush, the content of the message received is the content posted to the push + * gateway endpoint [3]. + * + * *Note*: If we want to get the same content with FCM and unifiedpush, we can do a new sygnal pusher [4]. + * + * [1] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py + * [2] https://github.com/matrix-org/sygnal/blob/main/sygnal/gcmpushkin.py#L366 + * [3] https://spec.matrix.org/latest/push-gateway-api/ + * [4] https://github.com/p1gp1g/sygnal/blob/unifiedpush/sygnal/upfcmpushkin.py (Not tested for a while) + */ + fun parseData(message: String, firebaseFormat: Boolean): PushData? { + val moshi = MatrixJsonParser.getMoshi() + return if (firebaseFormat) { + tryOrNull { moshi.adapter(PushDataFcm::class.java).fromJson(message) }?.toPushData() + } else { + tryOrNull { moshi.adapter(PushDataUnifiedPush::class.java).fromJson(message) }?.toPushData() + } + } +} 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 c89dc7a73c..91ab58207d 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 @@ -29,40 +29,47 @@ import kotlin.math.abs private const val DEFAULT_PUSHER_FILE_TAG = "mobile" class PushersManager @Inject constructor( + private val unifiedPushStore: UnifiedPushStore, private val activeSessionHolder: ActiveSessionHolder, private val localeProvider: LocaleProvider, private val stringProvider: StringProvider, - private val appNameProvider: AppNameProvider + private val appNameProvider: AppNameProvider, ) { - suspend fun testPush(pushKey: String) { + suspend fun testPush() { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - stringProvider.getString(R.string.pusher_http_url), + unifiedPushStore.getPushGateway()!!, stringProvider.getString(R.string.pusher_app_id), - pushKey, + unifiedPushStore.getEndpointOrToken().orEmpty(), TEST_EVENT_ID ) } fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { - val currentSession = activeSessionHolder.getActiveSession() - return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) + return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url)) } - suspend fun registerPusherWithFcmKey(pushKey: String) { + fun enqueueRegisterPusher( + pushKey: String, + gateway: String + ): UUID { val currentSession = activeSessionHolder.getActiveSession() - currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) + val pusher = createHttpPusher(pushKey, gateway) + return currentSession.pushersService().enqueueAddHttpPusher(pusher) } - private fun createHttpPusher(pushKey: String) = HttpPusher( + private fun createHttpPusher( + pushKey: String, + gateway: String + ) = 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 0000000000..724d3c7aa6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -0,0 +1,267 @@ +/* + * 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 androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.getApplicationLabel +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.BackgroundSyncMode +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.util.MatrixJsonParser +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import java.net.URL +import javax.inject.Inject + +class UnifiedPushHelper @Inject constructor( + private val context: Context, + private val unifiedPushStore: UnifiedPushStore, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences, + private val matrix: Matrix, + private val vectorFeatures: VectorFeatures, + private val fcmHelper: FcmHelper, +) { + fun register( + activity: FragmentActivity, + onDoneRunnable: Runnable? = null, + ) { + registerInternal( + activity, + onDoneRunnable = onDoneRunnable + ) + } + + fun reRegister( + activity: FragmentActivity, + pushersManager: PushersManager, + onDoneRunnable: Runnable? = null + ) { + registerInternal( + activity, + force = true, + pushersManager = pushersManager, + onDoneRunnable = onDoneRunnable + ) + } + + private fun registerInternal( + activity: FragmentActivity, + force: Boolean = false, + pushersManager: PushersManager? = null, + onDoneRunnable: Runnable? = null + ) { + activity.lifecycleScope.launch { + if (!vectorFeatures.allowExternalUnifiedPushDistributors()) { + UnifiedPush.saveDistributor(context, context.packageName) + UnifiedPush.registerApp(context) + onDoneRunnable?.run() + return@launch + } + if (force) { + // Un-register first + unregister(pushersManager) + } + if (UnifiedPush.getDistributor(context).isNotEmpty()) { + UnifiedPush.registerApp(context) + onDoneRunnable?.run() + return@launch + } + + // By default, use internal solution (fcm/background sync) + UnifiedPush.saveDistributor(context, context.packageName) + val distributors = UnifiedPush.getDistributors(context) + + if (distributors.size == 1 && !force) { + UnifiedPush.saveDistributor(context, distributors.first()) + UnifiedPush.registerApp(context) + onDoneRunnable?.run() + } else { + openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force) + } + } + } + + fun openDistributorDialog( + activity: FragmentActivity, + pushersManager: PushersManager, + onDoneRunnable: Runnable, + ) { + val distributors = UnifiedPush.getDistributors(activity) + openDistributorDialogInternal( + activity, + pushersManager, + onDoneRunnable, distributors, + unregisterFirst = true, + cancellable = true, + ) + } + + private fun openDistributorDialogInternal( + activity: FragmentActivity, + pushersManager: PushersManager?, + onDoneRunnable: Runnable?, + distributors: List, + unregisterFirst: Boolean, + cancellable: Boolean, + ) { + val internalDistributorName = stringProvider.getString( + if (fcmHelper.isFirebaseAvailable()) { + R.string.unifiedpush_distributor_fcm_fallback + } else { + R.string.unifiedpush_distributor_background_sync + } + ) + + val distributorsName = distributors.map { + if (it == context.packageName) { + internalDistributorName + } else { + context.getApplicationLabel(it) + } + } + + MaterialAlertDialogBuilder(activity) + .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) + .setItems(distributorsName.toTypedArray()) { _, which -> + val distributor = distributors[which] + if (distributor == UnifiedPush.getDistributor(context)) { + Timber.d("Same distributor selected again, no action") + return@setItems + } + + activity.lifecycleScope.launch { + if (unregisterFirst) { + // Un-register first + unregister(pushersManager) + } + UnifiedPush.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + UnifiedPush.registerApp(context) + onDoneRunnable?.run() + } + } + .setCancelable(cancellable) + .show() + } + + suspend fun unregister(pushersManager: PushersManager? = null) { + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + vectorPreferences.setFdroidSyncBackgroundMode(mode) + try { + pushersManager?.unregisterPusher(unifiedPushStore.getEndpointOrToken().orEmpty()) + } catch (e: Exception) { + Timber.d(e, "Probably unregistering a non existing pusher") + } + unifiedPushStore.storeUpEndpoint(null) + unifiedPushStore.storePushGateway(null) + UnifiedPush.unregisterApp(context) + } + + @JsonClass(generateAdapter = true) + internal data class DiscoveryResponse( + @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() + ) + + @JsonClass(generateAdapter = true) + internal data class DiscoveryUnifiedPush( + @Json(name = "gateway") val gateway: String = "" + ) + + suspend fun storeCustomOrDefaultGateway( + endpoint: String, + onDoneRunnable: Runnable? = null + ) { + // if we use the embedded distributor, + // register app_id type upfcm on sygnal + // the pushkey if FCM key + if (UnifiedPush.getDistributor(context) == context.packageName) { + unifiedPushStore.storePushGateway(stringProvider.getString(R.string.pusher_http_url)) + onDoneRunnable?.run() + return + } + // else, unifiedpush, and pushkey is an endpoint + val gateway = stringProvider.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") + try { + val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache) + val moshi = MatrixJsonParser.getMoshi() + moshi.adapter(DiscoveryResponse::class.java).fromJson(response) + ?.let { discoveryResponse -> + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + unifiedPushStore.storePushGateway(custom) + onDoneRunnable?.run() + return + } + } + } catch (e: Throwable) { + Timber.d(e, "Cannot try custom gateway") + } + unifiedPushStore.storePushGateway(gateway) + onDoneRunnable?.run() + } + + fun getExternalDistributors(): List { + return UnifiedPush.getDistributors(context) + .filterNot { it == context.packageName } + } + + fun getCurrentDistributorName(): String { + return when { + isEmbeddedDistributor() -> stringProvider.getString(R.string.unifiedpush_distributor_fcm_fallback) + isBackgroundSync() -> stringProvider.getString(R.string.unifiedpush_distributor_background_sync) + else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) + } + } + + fun isEmbeddedDistributor(): Boolean { + return UnifiedPush.getDistributor(context) == context.packageName && fcmHelper.isFirebaseAvailable() + } + + fun isBackgroundSync(): Boolean { + return UnifiedPush.getDistributor(context) == context.packageName && !fcmHelper.isFirebaseAvailable() + } + + fun getPrivacyFriendlyUpEndpoint(): String? { + val endpoint = unifiedPushStore.getEndpointOrToken() + if (endpoint.isNullOrEmpty()) return null + if (isEmbeddedDistributor()) { + return endpoint + } + return try { + val parsed = URL(endpoint) + "${parsed.protocol}://${parsed.host}/***" + } catch (e: Exception) { + Timber.e(e, "Error parsing unifiedpush endpoint") + null + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt new file mode 100644 index 0000000000..07d291a723 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushStore.kt @@ -0,0 +1,73 @@ +/* + * 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 androidx.core.content.edit +import im.vector.app.core.di.DefaultSharedPreferences +import javax.inject.Inject + +class UnifiedPushStore @Inject constructor( + context: Context, +) { + private val defaultPrefs = DefaultSharedPreferences.getInstance(context) + + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpointOrToken(): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs. + * + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(endpoint: String?) { + defaultPrefs.edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(): String? { + return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null) + } + + /** + * Store Push Gateway to the SharedPrefs. + * + * @param gateway the push gateway to store + */ + fun storePushGateway(gateway: String?) { + defaultPrefs.edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } + + companion object { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + } +} 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 57% 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 8d0126d6af..53a5470ff7 --- 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,11 +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, @@ -14,27 +14,28 @@ * 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 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.pushers.model.PushData +import im.vector.app.core.services.GuardServiceStarter 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 @@ -44,24 +45,28 @@ 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.matrix.android.sdk.api.session.room.getTimelineEvent +import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject 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 + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var unifiedPushStore: UnifiedPushStore + @Inject lateinit var pushParser: PushParser private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -73,22 +78,29 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { /** * Called when message is received. * + * @param context the Android context * @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() $sMessage") } - Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) runBlocking { vectorDataStore.incrementPushCounter() } + val pushData = pushParser.parseData(sMessage, unifiedPushHelper.isEmbeddedDistributor()) + ?: return Unit.also { Timber.tag(loggerTag.value).w("Invalid received data Json format") } + // Diagnostic Push - if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { + if (pushData.eventId == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) - LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) return } @@ -102,48 +114,64 @@ 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(pushData) + } + } + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") + if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { + // If the endpoint has changed + // or the gateway has changed + if (unifiedPushStore.getEndpointOrToken() != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint) + coroutineScope.launch { + unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { + unifiedPushStore.getPushGateway()?.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() + } + + 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(unifiedPushStore.getEndpointOrToken().orEmpty()) + } catch (e: Exception) { + Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher") } } } /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. Note that this is also called - * 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) - if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken) - } - } - - /** - * 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. + * Internal receive method. * - * It is recommended that the app do a full sync with the app server after receiving this call. + * @param pushData Object containing message data. */ - override fun onDeletedMessages() { - Timber.tag(loggerTag.value).v("## onDeletedMessages()") - } - - /** - * Internal receive method - * - * @param data Data map containing message data as key/value pairs. - * For Set of keys use data.keySet(). - */ - private fun onMessageReceivedInternal(data: Map) { + private fun onMessageReceivedInternal(pushData: PushData) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $pushData") } else { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } @@ -153,15 +181,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { 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(pushData)) { 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, pushData) Timber.tag(loggerTag.value).d("Requesting background sync") session.syncService().requireBackgroundSync() @@ -172,12 +197,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } } - private fun getEventFastLane(session: Session, roomId: String?, eventId: String?) { - roomId?.takeIf { it.isNotEmpty() } ?: return - eventId?.takeIf { it.isNotEmpty() } ?: return + private fun getEventFastLane(session: Session, pushData: PushData) { + pushData.roomId ?: return + pushData.eventId ?: return // If the room is currently displayed, we will not show a notification, so no need to get the Event faster - if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(roomId)) { + if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(pushData.roomId)) { return } @@ -188,7 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { coroutineScope.launch { Timber.tag(loggerTag.value).d("Fast lane: start request") - val event = tryOrNull { session.eventService().getEvent(roomId, eventId) } ?: return@launch + val event = tryOrNull { session.eventService().getEvent(pushData.roomId, pushData.eventId) } ?: return@launch val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) @@ -202,12 +227,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // check if the event was not yet received // a previous catchup might have already retrieved the notified event - private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean { - if (null != eventId && null != roomId) { + private fun isEventAlreadyKnown(pushData: PushData): Boolean { + if (pushData.eventId != null && pushData.roomId != null) { try { val session = activeSessionHolder.getSafeActiveSession() ?: return false - val room = session.getRoom(roomId) ?: return false - return room.getTimelineEvent(eventId) != null + val room = session.getRoom(pushData.roomId) ?: return false + return room.getTimelineEvent(pushData.eventId) != null } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined") } diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt new file mode 100644 index 0000000000..d1d095a6fa --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushData.kt @@ -0,0 +1,30 @@ +/* + * 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.model + +/** + * Represent parsed data that the app has received from a Push content. + * + * @property eventId The Event ID. If not null, it will not be empty, and will have a valid format. + * @property roomId The Room ID. If not null, it will not be empty, and will have a valid format. + * @property unread Number of unread message. + */ +data class PushData( + val eventId: String?, + val roomId: String?, + val unread: Int?, +) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt new file mode 100644 index 0000000000..1b9c37ae0a --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataFcm.kt @@ -0,0 +1,46 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.MatrixPatterns + +/** + * In this case, the format is: + *
+ * {
+ *     "event_id":"$anEventId",
+ *     "room_id":"!aRoomId",
+ *     "unread":"1",
+ *     "prio":"high"
+ * }
+ * 
+ * . + */ +@JsonClass(generateAdapter = true) +data class PushDataFcm( + @Json(name = "event_id") val eventId: String?, + @Json(name = "room_id") val roomId: String?, + @Json(name = "unread") var unread: Int?, +) + +fun PushDataFcm.toPushData() = PushData( + eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = unread +) diff --git a/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt new file mode 100644 index 0000000000..3dbd44f8ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/model/PushDataUnifiedPush.kt @@ -0,0 +1,60 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.MatrixPatterns + +/** + * In this case, the format is: + *
+ * {
+ *     "notification":{
+ *         "event_id":"$anEventId",
+ *         "room_id":"!aRoomId",
+ *         "counts":{
+ *             "unread":1
+ *         },
+ *         "prio":"high"
+ *     }
+ * }
+ * 
+ * . + */ +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPush( + @Json(name = "notification") val notification: PushDataUnifiedPushNotification? +) + +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPushNotification( + @Json(name = "event_id") val eventId: String?, + @Json(name = "room_id") val roomId: String?, + @Json(name = "counts") var counts: PushDataUnifiedPushCounts?, +) + +@JsonClass(generateAdapter = true) +data class PushDataUnifiedPushCounts( + @Json(name = "unread") val unread: Int? +) + +fun PushDataUnifiedPush.toPushData() = PushData( + eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }, + roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }, + unread = notification?.counts?.unread +) diff --git a/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt b/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt index 90558e35b7..3b6a8b595c 100644 --- a/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/AppNameProvider.kt @@ -17,6 +17,7 @@ package im.vector.app.core.resources import android.content.Context +import im.vector.app.core.utils.getApplicationLabel import timber.log.Timber import javax.inject.Inject @@ -25,9 +26,7 @@ class AppNameProvider @Inject constructor(private val context: Context) { fun getAppName(): String { return try { val appPackageName = context.applicationContext.packageName - val pm = context.packageManager - val appInfo = pm.getApplicationInfo(appPackageName, 0) - var appName = pm.getApplicationLabel(appInfo).toString() + var appName = context.getApplicationLabel(appPackageName) // Use appPackageName instead of appName if appName contains any non-ASCII character if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 1939bdf6a9..bb38411980 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -23,6 +23,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.PowerManager @@ -59,6 +60,18 @@ fun Context.isAnimationEnabled(): Boolean { return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f } +/** + * Return the application label of the provided package. If not found, the package is returned. + */ +fun Context.getApplicationLabel(packageName: String): String { + return try { + val ai = packageManager.getApplicationInfo(packageName, 0) + packageManager.getApplicationLabel(ai).toString() + } catch (e: PackageManager.NameNotFoundException) { + packageName + } +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 6a7a0865de..6fe4beff95 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -17,6 +17,7 @@ package im.vector.app.features import im.vector.app.BuildConfig +import im.vector.app.config.Config interface VectorFeatures { @@ -27,6 +28,7 @@ interface VectorFeatures { fun isOnboardingPersonalizeEnabled(): Boolean fun isOnboardingCombinedRegisterEnabled(): Boolean fun isOnboardingCombinedLoginEnabled(): Boolean + fun allowExternalUnifiedPushDistributors(): Boolean fun isScreenSharingEnabled(): Boolean enum class OnboardingVariant { @@ -44,5 +46,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false override fun isOnboardingCombinedLoginEnabled() = false + override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS override fun isScreenSharingEnabled(): Boolean = true } 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 fa991501ea..db03e7dc5d 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 @@ -72,7 +72,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP) class WebRtcCallManager @Inject constructor( private val context: Context, private val activeSessionDataSource: ActiveSessionDataSource, - private val analyticsTracker: AnalyticsTracker + private val analyticsTracker: AnalyticsTracker, + private val unifiedPushHelper: UnifiedPushHelper, ) : CallListener, DefaultLifecycleObserver { @@ -272,7 +273,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()) { currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing @@ -378,7 +379,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()) { // only for push version as fdroid version is already doing it? currentSession?.syncService()?.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 6f0e11f3b8..f2690fa18a 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 @@ -127,6 +128,8 @@ class HomeActivity : @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var appStateHandler: AppStateHandler + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var fcmHelper: FcmHelper private val createSpaceResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { @@ -187,7 +190,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()) { + 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 ce9c068c9c..276317b557 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 @@ -141,6 +141,9 @@ class VectorPreferences @Inject constructor( 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_NOTIFICATION_METHOD_KEY = "SETTINGS_NOTIFICATION_METHOD_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 2eb62bbb1e..62f5823b65 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 @@ -38,10 +38,12 @@ 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 import im.vector.app.core.utils.requestDisablingBatteryOptimization +import im.vector.app.features.VectorFeatures import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.settings.BackgroundSyncMode @@ -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,10 +63,12 @@ import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml class VectorSettingsNotificationPreferenceFragment @Inject constructor( - private val pushManager: PushersManager, + private val unifiedPushHelper: UnifiedPushHelper, + private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, - private val guardServiceStarter: GuardServiceStarter + private val guardServiceStarter: GuardServiceStarter, + private val vectorFeatures: VectorFeatures, ) : VectorSettingsBaseFragment(), BackgroundSyncModeChooserDialog.InteractionListener { @@ -98,14 +101,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(requireActivity()) { + // Update the summary + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() } } else { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.unregisterPusher(it) - session.pushersService().refreshPushers() - } + unifiedPushHelper.unregister(pushersManager) + session.pushersService().refreshPushers() } } } @@ -148,6 +151,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.let { + if (vectorFeatures.allowExternalUnifiedPushDistributors()) { + it.summary = unifiedPushHelper.getCurrentDistributorName() + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + unifiedPushHelper.openDistributorDialog(requireActivity(), pushersManager) { + it.summary = unifiedPushHelper.getCurrentDistributorName() + 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() } 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()) { 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 0000000000..acc0142924 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAvailableUnifiedPushDistributors.kt @@ -0,0 +1,49 @@ +/* + * 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 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 unifiedPushHelper: UnifiedPushHelper, + private val stringProvider: StringProvider, + private val fcmHelper: FcmHelper, +) : TroubleshootTest(R.string.settings_troubleshoot_test_distributors_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val distributors = unifiedPushHelper.getExternalDistributors() + description = if (distributors.isEmpty()) { + stringProvider.getString( + if (fcmHelper.isFirebaseAvailable()) { + R.string.settings_troubleshoot_test_distributors_gplay + } else { + R.string.settings_troubleshoot_test_distributors_fdroid + } + ) + } else { + val quantity = distributors.size + 1 + stringProvider.getQuantityString(R.plurals.settings_troubleshoot_test_distributors_many, quantity, quantity) + } + 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 0000000000..d43fb1bfe3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestCurrentUnifiedPushDistributor.kt @@ -0,0 +1,38 @@ +/* + * 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 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 unifiedPushHelper: UnifiedPushHelper, + 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() + ) + 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 0000000000..66222f759e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -0,0 +1,86 @@ +/* + * 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.pushers.UnifiedPushStore +import im.vector.app.core.resources.StringProvider +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 activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushStore: UnifiedPushStore, +) : 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 = unifiedPushStore.getEndpointOrToken() ?: 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 + ) + 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 83% 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 b4b8a936d0..cf2bf3d5f1 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,19 +13,16 @@ * 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 -import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder 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 @@ -34,28 +31,22 @@ import org.matrix.android.sdk.api.session.pushers.PushGatewayFailure import javax.inject.Inject /** - * Test Push by asking the Push Gateway to send a Push back + * Test Push by asking the Push Gateway to send a Push back. */ class TestPushFromPushGateway @Inject constructor( - private val context: FragmentActivity, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter, private val pushersManager: PushersManager, - private val activeSessionHolder: ActiveSessionHolder -) : - TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { + private val activeSessionHolder: ActiveSessionHolder, +) : TroubleshootTest(R.string.settings_troubleshoot_test_push_loop_title) { private var action: Job? = null private var pushReceived: Boolean = false 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() } 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 0000000000..a29d1ad812 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushEndpoint.kt @@ -0,0 +1,41 @@ +/* + * 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 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 stringProvider: StringProvider, + private val unifiedPushHelper: UnifiedPushHelper, +) : TroubleshootTest(R.string.settings_troubleshoot_test_current_endpoint_title) { + + override fun perform(activityResultLauncher: ActivityResultLauncher) { + val endpoint = unifiedPushHelper.getPrivacyFriendlyUpEndpoint() + if (endpoint != null) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_current_endpoint_success, endpoint) + status = TestStatus.SUCCESS + } else { + 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 0000000000..38f14951b4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestUnifiedPushGateway.kt @@ -0,0 +1,38 @@ +/* + * 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 im.vector.app.R +import im.vector.app.core.pushers.UnifiedPushStore +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class TestUnifiedPushGateway @Inject constructor( + private val unifiedPushStore: UnifiedPushStore, + 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, + unifiedPushStore.getPushGateway() + ) + status = TestStatus.SUCCESS + } +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a7d087e71b..f60da53c09 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -855,6 +855,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 @@ -1665,6 +1669,8 @@ Register token + Reset notification method + Make a suggestion Please write your suggestion below. Describe your suggestion here @@ -3063,4 +3069,23 @@ ${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 method. + 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 66ac93a4f9..f4d7ff8cd5 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -51,6 +51,12 @@ android:persistent="false" android:title="@string/settings_notification_configuration"> + + - \ No newline at end of file + diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt new file mode 100644 index 0000000000..03577a4400 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/PushParserTest.kt @@ -0,0 +1,107 @@ +/* + * 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 im.vector.app.core.pushers.model.PushData +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class PushParserTest { + private val validData = PushData( + eventId = "\$anEventId", + roomId = "!aRoomId:domain", + unread = 1 + ) + + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null + ) + + @Test + fun `test edge cases`() { + doAllEdgeTests(true) + doAllEdgeTests(false) + } + + private fun doAllEdgeTests(firebaseFormat: Boolean) { + val pushParser = PushParser() + // Empty string + pushParser.parseData("", firebaseFormat) shouldBe null + // Empty Json + pushParser.parseData("{}", firebaseFormat) shouldBeEqualTo emptyData + // Bad Json + pushParser.parseData("ABC", firebaseFormat) shouldBe null + } + + @Test + fun `test unified push format`() { + val pushParser = PushParser() + + pushParser.parseData(UNIFIED_PUSH_DATA, false) shouldBeEqualTo validData + pushParser.parseData(UNIFIED_PUSH_DATA, true) shouldBeEqualTo emptyData + } + + @Test + fun `test firebase push format`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA, true) shouldBeEqualTo validData + pushParser.parseData(FIREBASE_PUSH_DATA, false) shouldBeEqualTo emptyData + } + + @Test + fun `test empty roomId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId:domain", ""), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId:domain", ""), false) shouldBeEqualTo validData.copy(roomId = null) + } + + @Test + fun `test invalid roomId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("!aRoomId:domain", "aRoomId:domain"), true) shouldBeEqualTo validData.copy(roomId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("!aRoomId:domain", "aRoomId:domain"), false) shouldBeEqualTo validData.copy(roomId = null) + } + + @Test + fun `test empty eventId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", ""), true) shouldBeEqualTo validData.copy(eventId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", ""), false) shouldBeEqualTo validData.copy(eventId = null) + } + + @Test + fun `test invalid eventId`() { + val pushParser = PushParser() + + pushParser.parseData(FIREBASE_PUSH_DATA.replace("\$anEventId", "anEventId"), true) shouldBeEqualTo validData.copy(eventId = null) + pushParser.parseData(UNIFIED_PUSH_DATA.replace("\$anEventId", "anEventId"), false) shouldBeEqualTo validData.copy(eventId = null) + } + + companion object { + private const val UNIFIED_PUSH_DATA = + "{\"notification\":{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId:domain\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + private const val FIREBASE_PUSH_DATA = + "{\"event_id\":\"\$anEventId\",\"room_id\":\"!aRoomId:domain\",\"unread\":\"1\",\"prio\":\"high\"}" + } +}