diff --git a/changelog.d/3448.feature b/changelog.d/3448.feature new file mode 100644 index 0000000000..3f83f1bef5 --- /dev/null +++ b/changelog.d/3448.feature @@ -0,0 +1 @@ +Use UnifiedPush and allows user to have push without FCM. diff --git a/dependencies.gradle b/dependencies.gradle index 451ad4449b..0b29996438 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,6 +43,7 @@ ext.libs = [ ], jetbrains : [ + 'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$kotlin", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" @@ -88,6 +89,7 @@ ext.libs = [ ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", + 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 59cefe7e89..842a235b16 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -12,6 +12,7 @@ ext.groups = [ 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', + 'com.github.UnifiedPush', ] ], jitsi : [ diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index 78b92cbfa4..cae094f454 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -17,7 +17,11 @@ --> + https://matrix.org/_matrix/push/v1/notify + + + https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify im.vector.app.android diff --git a/vector/build.gradle b/vector/build.gradle index 751d15122b..238a9f05d0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -282,6 +282,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } fdroid { @@ -293,6 +294,7 @@ android { buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" + buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true" } } @@ -348,6 +350,7 @@ dependencies { implementation project(":library:multipicker") implementation 'androidx.multidex:multidex:2.0.1' + implementation libs.jetbrains.kotlinReflect implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid @@ -364,6 +367,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi + implementation libs.squareup.moshiKt kapt libs.squareup.moshiKotlin // Lifecycle @@ -462,8 +466,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-beta2' + // UnifiedPush gplay flavor only + gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:a0056aa939') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml index f541eebd83..5b384d1a0a 100755 --- a/vector/src/gplay/AndroidManifest.xml +++ b/vector/src/gplay/AndroidManifest.xml @@ -9,13 +9,17 @@ android:name="firebase_analytics_collection_deactivated" android:value="true" /> - + - + + - + + diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt index b4b8a936d0..3fd80ad1c5 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestPushFromPushGateway.kt @@ -50,12 +50,12 @@ class TestPushFromPushGateway @Inject constructor( override fun perform(activityResultLauncher: ActivityResultLauncher) { pushReceived = false - val fcmToken = FcmHelper.getFcmToken(context) ?: run { + FcmHelper.getFcmToken(context) ?: run { status = TestStatus.FAILED return } action = activeSessionHolder.getActiveSession().coroutineScope.launch { - val result = runCatching { pushersManager.testPush(fcmToken) } + val result = runCatching { pushersManager.testPush(context) } withContext(Dispatchers.Main) { status = result diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt new file mode 100644 index 0000000000..0d0e066eb4 --- /dev/null +++ b/vector/src/gplay/java/im/vector/app/push/fcm/EmbeddedFCMDistributor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.push.fcm + +import android.content.Context +import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver + +class EmbeddedFCMDistributor: EmbeddedDistributorReceiver() { + override fun getEndpoint(context: Context, token: String, instance: String): String { + // Here token is the FCM Token, used by the gateway (sygnal) + return token + } +} diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index ac2d063700..ef333bb30b 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -98,9 +98,9 @@ object FcmHelper { * it doesn't, display a dialog that allows users to download the APK from * the Google Play Store or enable it in the device's system settings. */ - private fun checkPlayServices(activity: Activity): Boolean { + fun checkPlayServices(context: Context): Boolean { val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity) + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) return resultCode == ConnectionResult.SUCCESS } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 89c600b052..87fd307647 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -410,6 +410,17 @@ + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index c89dc7a73c..a5e03c7d14 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -16,6 +16,7 @@ package im.vector.app.core.pushers +import android.content.Context import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider @@ -34,13 +35,13 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider ) { - suspend fun testPush(pushKey: String) { + suspend fun testPush(context: Context) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().testPush( - stringProvider.getString(R.string.pusher_http_url), + UnifiedPushHelper.getPushGateway(context), stringProvider.getString(R.string.pusher_app_id), - pushKey, + UnifiedPushHelper.getEndpointOrToken(context) ?: "", TEST_EVENT_ID ) } @@ -50,19 +51,38 @@ class PushersManager @Inject constructor( return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) } + fun enqueueRegisterPusher( + pushKey: String, + gateway: String + ): UUID { + val currentSession = activeSessionHolder.getActiveSession() + return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey, gateway)) + } + suspend fun registerPusherWithFcmKey(pushKey: String) { val currentSession = activeSessionHolder.getActiveSession() currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) } - private fun createHttpPusher(pushKey: String) = HttpPusher( + suspend fun registerPusher( + pushKey: String, + gateway: String + ) { + val currentSession = activeSessionHolder.getActiveSession() + currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey, gateway)) + } + + private fun createHttpPusher( + pushKey: String, + gateway: String = stringProvider.getString(R.string.pusher_http_url) + ) = HttpPusher( pushKey, stringProvider.getString(R.string.pusher_app_id), profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), localeProvider.current().language, appNameProvider.getAppName(), activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", - stringProvider.getString(R.string.pusher_http_url), + gateway, append = false, withEventIdOnly = true ) diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt new file mode 100644 index 0000000000..62b174b0f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.pushers + +import android.content.Context +import android.content.pm.PackageManager +import androidx.appcompat.app.AlertDialog +import androidx.core.content.edit +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.core.di.DefaultSharedPreferences +import im.vector.app.push.fcm.FcmHelper +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import java.net.URI +import java.net.URL + +object UnifiedPushHelper { + private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN" + private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY" + private val up = UnifiedPush + + /** + * Retrieves the UnifiedPush Endpoint. + * + * @return the UnifiedPush Endpoint or null if not received + */ + fun getEndpointOrToken(context: Context): String? { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_ENDPOINT_OR_TOKEN, null) + } + + /** + * Store UnifiedPush Endpoint to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param endpoint the endpoint to store + */ + fun storeUpEndpoint(context: Context, + endpoint: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + } + } + + /** + * Retrieves the Push Gateway. + * + * @return the Push Gateway or null if not defined + */ + fun getPushGateway(context: Context): String { + return DefaultSharedPreferences.getInstance(context).getString(PREFS_PUSH_GATEWAY, null)!! + } + + /** + * Store Push Gateway to the SharedPrefs + * TODO Store in realm + * + * @param context android context + * @param gateway the push gateway to store + */ + fun storePushGateway(context: Context, + gateway: String?) { + DefaultSharedPreferences.getInstance(context).edit { + putString(PREFS_PUSH_GATEWAY, gateway) + } + } + + fun register(context: Context, force: Boolean = false, onDoneRunnable: Runnable? = null) { + if (!BuildConfig.ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB) { + up.saveDistributor(context, context.packageName) + up.registerApp(context) + onDoneRunnable?.run() + return + } + if (force) { + // Un-register first + up.unregisterApp(context) + storeUpEndpoint(context, null) + storePushGateway(context, null) + } else if (up.getDistributor(context).isNotEmpty()) { + up.registerApp(context) + onDoneRunnable?.run() + return + } + val distributors = up.getDistributors(context).toMutableList() + + val internalDistributorName = if (!FcmHelper.isPushSupported()) { + // Adding packageName for background sync + distributors.add(context.packageName) + context.getString(R.string.unifiedpush_getdistributors_dialog_background_sync) + } else { + context.getString(R.string.unifiedpush_getdistributors_dialog_fcm_fallback) + } + + if (distributors.size == 1 + && !force){ + up.saveDistributor(context, distributors.first()) + up.registerApp(context) + onDoneRunnable?.run() + } else { + val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(context) + builder.setTitle(context.getString(R.string.unifiedpush_getdistributors_dialog_title)) + + val distributorsArray = distributors.toTypedArray() + val distributorsNameArray = distributorsArray.map { + if (it == context.packageName) { + internalDistributorName + } else { + try { + val ai = context.packageManager.getApplicationInfo(it, 0) + context.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + } + }.toTypedArray() + builder.setItems(distributorsNameArray) { _, which -> + val distributor = distributorsArray[which] + up.saveDistributor(context, distributor) + Timber.i("Saving distributor: $distributor") + up.registerApp(context) + onDoneRunnable?.run() + } + builder.setOnDismissListener { + onDoneRunnable?.run() + } + builder.setOnCancelListener { + onDoneRunnable?.run() + } + val dialog: AlertDialog = builder.create() + dialog.show() + } + } + + fun unregister(context: Context) { + up.unregisterApp(context) + } + + fun customOrDefaultGateway(context: Context, endpoint: String?): String { + // if we use the embedded distributor, + // register app_id type upfcm on sygnal + // the pushkey if FCM key + if (up.getDistributor(context) == context.packageName) { + return context.getString(R.string.pusher_http_url) + } + // else, unifiedpush, and pushkey is an endpoint + val default = context.getString(R.string.default_push_gateway_http_url) + endpoint?.let { + val uri = URI(it) + val custom = "${it.split(uri.rawPath)[0]}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + /** + * TODO: + * if GET custom returns """{"unifiedpush":{"gateway":"matrix"}}""" + * return custom + */ + } + return default + } + + fun distributorExists(context: Context): Boolean { + return up.getDistributor(context).isNotEmpty() + } + + fun isEmbeddedDistributor(context: Context) : Boolean { + return ( up.getDistributor(context) == context.packageName + && FcmHelper.isPushSupported()) + } + + fun isBackgroundSync(context: Context) : Boolean { + return ( up.getDistributor(context) == context.packageName + && !FcmHelper.isPushSupported()) + } + + fun getPrivacyFriendlyUpEndpoint(context: Context): String? { + val endpoint = getEndpointOrToken(context) + if (endpoint.isNullOrEmpty()) return endpoint + if (isEmbeddedDistributor(context)) { + return endpoint + } + return try { + val parsed = URL(endpoint) + "${parsed.protocol}://${parsed.host}" + } catch (e: Exception) { + Timber.e("Error parsing unifiedpush endpoint: $e") + null + } + } +} diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt old mode 100755 new mode 100644 similarity index 60% rename from vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt rename to vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 8d0126d6af..4b30f802e3 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2022 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,27 +14,31 @@ * limitations under the License. */ -package im.vector.app.gplay.push.fcm +package im.vector.app.core.pushers +import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper +import android.widget.Toast import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector -import im.vector.app.core.pushers.PushersManager +import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -43,18 +47,35 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject +@JsonClass(generateAdapter = true) +data class UnifiedPushMessage( + val notification: Notification = Notification() +) + +@JsonClass(generateAdapter = true) +data class Notification( + @Json(name = "event_id") val eventId: String = "", + @Json(name = "room_id") val roomId: String = "", + var unread: Int = 0, + val counts: Counts = Counts() +) + +@JsonClass(generateAdapter = true) +data class Counts( + val unread: Int = 0 +) + private val loggerTag = LoggerTag("Push", LoggerTag.SYNC) /** - * Class extending FirebaseMessagingService. + * Hilt injection happen at super.onReceive(). */ @AndroidEntryPoint -class VectorFirebaseMessagingService : FirebaseMessagingService() { - +class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notifiableEventResolver: NotifiableEventResolver @Inject lateinit var pusherManager: PushersManager @@ -74,21 +95,38 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * Called when message is received. * * @param message the message + * @param instance connection, for multi-account */ - override fun onMessageReceived(message: RemoteMessage) { + override fun onMessage(context: Context, message: ByteArray, instance: String) { + Timber.tag(loggerTag.value).d("## onMessage() received") + val sMessage = String(message) if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceived() %s", message.data.toString()) + Timber.tag(loggerTag.value).d("## onMessage() %s", sMessage) } - Timber.tag(loggerTag.value).d("## onMessageReceived() from FCM with priority %s", message.priority) runBlocking { vectorDataStore.incrementPushCounter() } + val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + lateinit var notification: Notification + + if (UnifiedPushHelper.isEmbeddedDistributor(context)) { + notification = moshi.adapter(Notification::class.java) + .fromJson(sMessage) ?: return + } else { + val data = moshi.adapter(UnifiedPushMessage::class.java) + .fromJson(sMessage) ?: return + notification = data.notification + notification.unread = notification.counts.unread + } + // Diagnostic Push - if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { + if (notification.eventId == PushersManager.TEST_EVENT_ID) { val intent = Intent(NotificationUtils.PUSH_ACTION) - LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) return } @@ -102,7 +140,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // we are in foreground, let the sync do the things? Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") } else { - onMessageReceivedInternal(message.data) + onMessageReceivedInternal(context, notification) } } } @@ -113,55 +151,69 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * when the InstanceID token is initially generated, so this is where * you retrieve the token. */ - override fun onNewToken(refreshedToken: String) { - Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") - FcmHelper.storeFcmToken(this, refreshedToken) + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken) + val gateway = UnifiedPushHelper.customOrDefaultGateway(context, endpoint) + // If the endpoint has changed + // or the gateway has changed + if (UnifiedPushHelper.getEndpointOrToken(context) != endpoint + || UnifiedPushHelper.getPushGateway(context) != gateway) { + UnifiedPushHelper.storePushGateway(context, gateway) + UnifiedPushHelper.storeUpEndpoint(context, endpoint) + pusherManager.enqueueRegisterPusher(endpoint, gateway) + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") + } } + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + vectorPreferences.setFdroidSyncBackgroundMode(mode) } - /** - * Called when the FCM server deletes pending messages. This may be due to: - * - Too many messages stored on the FCM server. - * This can occur when an app's servers send a bunch of non-collapsible messages to FCM servers while the device is offline. - * - The device hasn't connected in a long time and the app server has recently (within the last 4 weeks) - * sent a message to the app on that device. - * - * It is recommended that the app do a full sync with the app server after receiving this call. - */ - override fun onDeletedMessages() { - Timber.tag(loggerTag.value).v("## onDeletedMessages()") + override fun onRegistrationFailed(context: Context, instance: String) { + Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() + } + + override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") + val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + vectorPreferences.setFdroidSyncBackgroundMode(mode) + runBlocking { + try { + pusherManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "") + } catch (e: Exception) { + Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher") + } + } } /** * Internal receive method * - * @param data Data map containing message data as key/value pairs. - * For Set of keys use data.keySet(). + * @param notification Notification containing message data. */ - private fun onMessageReceivedInternal(data: Map) { + private fun onMessageReceivedInternal(context: Context, notification: Notification) { try { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") + Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification") } else { Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") } + // update the badge counter + BadgeProxy.updateBadgeCount(context, notification.unread) + val session = activeSessionHolder.getSafeActiveSession() if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") } else { - val eventId = data["event_id"] - val roomId = data["room_id"] - - if (isEventAlreadyKnown(eventId, roomId)) { + if (isEventAlreadyKnown(notification.eventId, notification.roomId)) { Timber.tag(loggerTag.value).d("Ignoring push, event already known") } else { // Try to get the Event content faster Timber.tag(loggerTag.value).d("Requesting event in fast lane") - getEventFastLane(session, roomId, eventId) + getEventFastLane(session, notification.roomId, notification.eventId) Timber.tag(loggerTag.value).d("Requesting background sync") session.syncService().requireBackgroundSync() diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index fa991501ea..cf532ea744 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.CallService import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.CallEnded @@ -32,7 +33,6 @@ import im.vector.app.features.call.lookup.CallUserMapper import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.vectorCallService import im.vector.app.features.session.coroutineScope -import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.extensions.orFalse @@ -272,7 +272,7 @@ class WebRtcCallManager @Inject constructor( audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing @@ -378,7 +378,7 @@ class WebRtcCallManager @Inject constructor( // and thus won't be able to received events. For example if the call is // accepted on an other session this device will continue ringing if (isInBackground) { - if (FcmHelper.isPushSupported()) { + if (!UnifiedPushHelper.isBackgroundSync(context)) { // only for push version as fdroid version is already doing it? currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 6f0e11f3b8..24831a67e3 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -44,6 +44,7 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs @@ -187,7 +188,15 @@ class HomeActivity : super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) + UnifiedPushHelper.register(this, onDoneRunnable = { + if (UnifiedPushHelper.isEmbeddedDistributor(this)) { + FcmHelper.ensureFcmTokenIsRetrieved( + this, + pushManager, + vectorPreferences.areNotificationEnabledForDevice() + ) + } + }) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) views.drawerLayout.addDrawerListener(drawerListener) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 2eb62bbb1e..6a40dd2311 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -38,6 +38,7 @@ import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.utils.combineLatest import im.vector.app.core.utils.isIgnoringBatteryOptimizations @@ -49,7 +50,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 @@ -58,6 +58,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.RuleKind +import timber.log.Timber import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -97,16 +98,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.registerPusherWithFcmKey(it) - } - } else { - FcmHelper.getFcmToken(requireContext())?.let { - pushManager.unregisterPusher(it) - session.pushersService().refreshPushers() - } - } + updateEnabledForDevice(isChecked) } } @@ -222,7 +214,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } findPreference(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { - it.isVisible = !FcmHelper.isPushSupported() + it.isVisible = UnifiedPushHelper.isBackgroundSync(requireContext()) } val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled() @@ -331,7 +323,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private fun refreshPref() { // This pref may have change from troubleshoot pref fragment - if (!FcmHelper.isPushSupported()) { + if (UnifiedPushHelper.isBackgroundSync(requireContext())) { findPreference(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) ?.isChecked = vectorPreferences.autoStartOnBoot() } @@ -364,6 +356,26 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( } } + private suspend fun updateEnabledForDevice(enabled: Boolean) { + if (enabled) { + UnifiedPushHelper.register(requireContext()) + } else { + UnifiedPushHelper.getEndpointOrToken(requireContext())?.let { + try { + pushManager.unregisterPusher(it) + } catch (e: Exception) { + Timber.d("Probably unregistering a non existant pusher") + } + try { + UnifiedPushHelper.unregister(requireContext()) + } catch (e: Exception) { + Timber.d("Probably unregistering to a non-saved distributor") + } + session.pushersService().refreshPushers() + } + } + } + private fun updateEnabledForAccount(preference: Preference?) { val pushRuleService = session.pushRuleService() val switchPref = preference as SwitchPreference diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 97ef900ce3..71ed348094 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3063,4 +3063,8 @@ ${app_name} Screen Sharing Screen sharing is in progress + + Choose how to receive notifications + Google Services + Background synchronization