Merge pull request #6228 from vector-im/feature/bma/unifiedPush2

UnifiedPush
This commit is contained in:
Benoit Marty 2022-06-16 16:09:31 +02:00 committed by GitHub
commit 16ca26569f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1451 additions and 190 deletions

View File

@ -40,6 +40,7 @@
<w>sygnal</w>
<w>threepid</w>
<w>uisi</w>
<w>unifiedpush</w>
<w>unpublish</w>
<w>unwedging</w>
<w>vctr</w>

1
changelog.d/3448.feature Normal file
View File

@ -0,0 +1 @@
Use UnifiedPush and allows user to have push without FCM.

View File

@ -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"

View File

@ -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',

58
docs/unifiedpush.md Normal file
View File

@ -0,0 +1,58 @@
# UnifiedPush
<!--- TOC -->
* [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)
<!--- END -->
## 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)

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -17,7 +17,11 @@
-->
<!-- Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
<!-- It is the push gateway for FCM embedded distributor -->
<string name="pusher_http_url" translatable="false">https://matrix.org/_matrix/push/v1/notify</string>
<!-- Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify' -->
<!-- It is the push gateway for UnifiedPush -->
<string name="default_push_gateway_http_url" translatable="false">https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify</string>
<!-- Note: pusher_app_id cannot exceed 64 chars -->
<string name="pusher_app_id" translatable="false">im.vector.app.android</string>

View File

@ -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'

View File

@ -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()
}
}

View File

@ -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
),
)
)
}

View File

@ -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")
}

View File

@ -28,6 +28,20 @@
android:enabled="true"
android:exported="false" />
<receiver
android:name=".fdroid.receiver.KeepInternalDistributor"
android:enabled="true"
android:exported="false">
<intent-filter>
<!--
This action is checked to track installed and uninstalled distributors.
We declare it to keep the background sync as an internal
unifiedpush distributor.
-->
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
</intent-filter>
</receiver>
<service
android:name=".fdroid.service.GuardService"
android:exported="false"
@ -35,4 +49,4 @@
</application>
</manifest>
</manifest>

View File

@ -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()) {

View File

@ -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) {}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -9,13 +9,17 @@
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<service
android:name=".gplay.push.fcm.VectorFirebaseMessagingService"
<receiver
android:enabled="true"
android:name=".push.fcm.EmbeddedFCMDistributor"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
<action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
</intent-filter>
</service>
</receiver>
</application>

View File

@ -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<Intent>) {
@ -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
}

View File

@ -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<Intent>) {
// 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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -304,7 +304,8 @@
android:supportsPictureInPicture="true" />
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity"
<activity
android:name=".features.widgets.WidgetActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
<activity android:name=".features.pin.PinActivity" />
@ -410,6 +411,20 @@
</intent-filter>
</receiver>
<!-- UnifiedPush -->
<receiver
android:exported="true"
android:enabled="true"
android:name=".core.pushers.VectorMessagingReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
</intent-filter>
</receiver>
<!-- Providers -->
<!-- Remove WorkManagerInitializer Provider because we are using on-demand initialization of WorkManager-->

View File

@ -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)

View File

@ -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()
}

View File

@ -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()
}
}
}

View File

@ -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
)

View File

@ -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<String>,
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<String> {
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
}
}
}

View File

@ -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"
}
}

View File

@ -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<String, String>) {
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")
}

View File

@ -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?,
)

View File

@ -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:
* <pre>
* {
* "event_id":"$anEventId",
* "room_id":"!aRoomId",
* "unread":"1",
* "prio":"high"
* }
* </pre>
* .
*/
@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
)

View File

@ -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:
* <pre>
* {
* "notification":{
* "event_id":"$anEventId",
* "room_id":"!aRoomId",
* "counts":{
* "unread":1
* },
* "prio":"high"
* }
* }
* </pre>
* .
*/
@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
)

View File

@ -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())) {

View File

@ -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).

View File

@ -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
}

View File

@ -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 {

View File

@ -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()) {

View File

@ -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"

View File

@ -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<SwitchPreference>(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<VectorPreference>(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<VectorPreference>(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<VectorPreferenceCategory>(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<VectorSwitchPreference>(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY)
?.isChecked = vectorPreferences.autoStartOnBoot()
}

View File

@ -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<Intent>) {
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
}
}

View File

@ -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<Intent>) {
description = stringProvider.getString(
R.string.settings_troubleshoot_test_current_distributor,
unifiedPushHelper.getCurrentDistributorName()
)
status = TestStatus.SUCCESS
}
}

View File

@ -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<Intent>) {
// 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
}
}
}

View File

@ -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<Intent>) {
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

View File

@ -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<Intent>) {
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
}
}
}

View File

@ -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<Intent>) {
description = stringProvider.getString(
R.string.settings_troubleshoot_test_current_gateway,
unifiedPushStore.getPushGateway()
)
status = TestStatus.SUCCESS
}
}

View File

@ -855,6 +855,10 @@
<string name="settings_troubleshoot_test_token_registration_success">FCM token successfully registered to homeserver.</string>
<string name="settings_troubleshoot_test_token_registration_failed">Failed to register FCM token to homeserver:\n%1$s</string>
<string name="settings_troubleshoot_test_endpoint_registration_title">Endpoint Registration</string>
<string name="settings_troubleshoot_test_endpoint_registration_success">Endpoint successfully registered to homeserver.</string>
<string name="settings_troubleshoot_test_endpoint_registration_failed">Failed to register endpoint token to homeserver:\n%1$s</string>
<string name="settings_troubleshoot_test_push_loop_title">Test Push</string>
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">The application is waiting for the PUSH</string>
<string name="settings_troubleshoot_test_push_loop_success">The application is receiving PUSH</string>
@ -1665,6 +1669,8 @@
<string name="settings_troubleshoot_test_token_registration_quick_fix">Register token</string>
<string name="settings_troubleshoot_test_endpoint_registration_quick_fix">Reset notification method</string>
<string name="send_suggestion">Make a suggestion</string>
<string name="send_suggestion_content">Please write your suggestion below.</string>
<string name="send_suggestion_report_placeholder">Describe your suggestion here</string>
@ -3063,4 +3069,23 @@
<!-- Screen sharing -->
<string name="screen_sharing_notification_title">${app_name} Screen Sharing</string>
<string name="screen_sharing_notification_description">Screen sharing is in progress</string>
<string name="unifiedpush_getdistributors_dialog_title">Choose how to receive notifications</string>
<string name="unifiedpush_distributor_fcm_fallback">Google Services</string>
<string name="unifiedpush_distributor_background_sync">Background synchronization</string>
<string name="settings_notification_method">Notification method</string>
<string name="settings_troubleshoot_test_distributors_title">Available methods</string>
<string name="settings_troubleshoot_test_distributors_gplay">No other method than Google Play Service found.</string>
<string name="settings_troubleshoot_test_distributors_fdroid">No other method than background synchronization found.</string>
<plurals name="settings_troubleshoot_test_distributors_many">
<item quantity="one">Found %d method.</item>
<item quantity="other">Found %d methods.</item>
</plurals>
<string name="settings_troubleshoot_test_current_distributor_title">Method</string>
<string name="settings_troubleshoot_test_current_distributor">Currently using %s.</string>
<string name="settings_troubleshoot_test_current_endpoint_title">Endpoint</string>
<string name="settings_troubleshoot_test_current_endpoint_success">Current endpoint: %s</string>
<string name="settings_troubleshoot_test_current_endpoint_failed">Cannot find the endpoint.</string>
<string name="settings_troubleshoot_test_current_gateway_title">Gateway</string>
<string name="settings_troubleshoot_test_current_gateway">Current gateway: %s</string>
</resources>

View File

@ -51,6 +51,12 @@
android:persistent="false"
android:title="@string/settings_notification_configuration">
<im.vector.app.core.preference.VectorPreference
android:dependency="SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"
android:key="SETTINGS_NOTIFICATION_METHOD_KEY"
android:persistent="false"
android:title="@string/settings_notification_method" />
<!-- For API < 26 -->
<im.vector.app.core.preference.VectorPreference
android:dialogTitle="@string/settings_notification_ringtone"
@ -121,4 +127,4 @@
android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /-->
</androidx.preference.PreferenceScreen>
</androidx.preference.PreferenceScreen>

View File

@ -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\"}"
}
}