Add UnifiedPush support

This commit is contained in:
sim 2022-02-25 16:25:56 +01:00 committed by Benoit Marty
parent 928183ff64
commit 04b297b261
17 changed files with 431 additions and 73 deletions

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

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

View file

@ -43,6 +43,7 @@ ext.libs = [
], ],
jetbrains : [ jetbrains : [
'kotlinReflect' : "org.jetbrains.kotlin:kotlin-reflect:$kotlin",
'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines",
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
@ -88,6 +89,7 @@ ext.libs = [
], ],
squareup : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi", 'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit", 'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"

View file

@ -12,6 +12,7 @@ ext.groups = [
'com.github.vector-im', 'com.github.vector-im',
'com.github.yalantis', 'com.github.yalantis',
'com.github.Zhuinden', 'com.github.Zhuinden',
'com.github.UnifiedPush',
] ]
], ],
jitsi : [ jitsi : [

View file

@ -17,7 +17,11 @@
--> -->
<!-- Note: pusher_http_url should have path '/_matrix/push/v1/notify' --> <!-- 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> <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 --> <!-- Note: pusher_app_id cannot exceed 64 chars -->
<string name="pusher_app_id" translatable="false">im.vector.app.android</string> <string name="pusher_app_id" translatable="false">im.vector.app.android</string>

View file

@ -282,6 +282,7 @@ android {
buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "boolean", "ALLOW_FCM_USE", "true"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true"
} }
fdroid { fdroid {
@ -293,6 +294,7 @@ android {
buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "boolean", "ALLOW_FCM_USE", "false"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true"
} }
} }
@ -348,6 +350,7 @@ dependencies {
implementation project(":library:multipicker") implementation project(":library:multipicker")
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation libs.jetbrains.kotlinReflect
implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid implementation libs.jetbrains.coroutinesAndroid
@ -364,6 +367,7 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
implementation libs.squareup.moshi implementation libs.squareup.moshi
implementation libs.squareup.moshiKt
kapt libs.squareup.moshiKotlin kapt libs.squareup.moshiKotlin
// Lifecycle // Lifecycle
@ -462,8 +466,10 @@ dependencies {
// Analytics // Analytics
implementation 'com.posthog.android:posthog:1.1.2' implementation 'com.posthog.android:posthog:1.1.2'
// gplay flavor only // UnifiedPush
gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { 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-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'

View file

@ -9,13 +9,17 @@
android:name="firebase_analytics_collection_deactivated" android:name="firebase_analytics_collection_deactivated"
android:value="true" /> android:value="true" />
<service <receiver
android:name=".gplay.push.fcm.VectorFirebaseMessagingService" android:enabled="true"
android:name=".push.fcm.EmbeddedFCMDistributor"
android:exported="false"> android:exported="false">
<intent-filter> <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> </intent-filter>
</service>
</receiver>
</application> </application>

View file

@ -50,12 +50,12 @@ class TestPushFromPushGateway @Inject constructor(
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) { override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
pushReceived = false pushReceived = false
val fcmToken = FcmHelper.getFcmToken(context) ?: run { FcmHelper.getFcmToken(context) ?: run {
status = TestStatus.FAILED status = TestStatus.FAILED
return return
} }
action = activeSessionHolder.getActiveSession().coroutineScope.launch { action = activeSessionHolder.getActiveSession().coroutineScope.launch {
val result = runCatching { pushersManager.testPush(fcmToken) } val result = runCatching { pushersManager.testPush(context) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
status = result status = result

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

@ -98,9 +98,9 @@ object FcmHelper {
* it doesn't, display a dialog that allows users to download the APK from * 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. * 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 apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity) val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
return resultCode == ConnectionResult.SUCCESS return resultCode == ConnectionResult.SUCCESS
} }

View file

@ -410,6 +410,17 @@
</intent-filter> </intent-filter>
</receiver> </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 --> <!-- Providers -->
<!-- Remove WorkManagerInitializer Provider because we are using on-demand initialization of WorkManager--> <!-- Remove WorkManagerInitializer Provider because we are using on-demand initialization of WorkManager-->

View file

@ -16,6 +16,7 @@
package im.vector.app.core.pushers package im.vector.app.core.pushers
import android.content.Context
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.AppNameProvider
@ -34,13 +35,13 @@ class PushersManager @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val appNameProvider: AppNameProvider private val appNameProvider: AppNameProvider
) { ) {
suspend fun testPush(pushKey: String) { suspend fun testPush(context: Context) {
val currentSession = activeSessionHolder.getActiveSession() val currentSession = activeSessionHolder.getActiveSession()
currentSession.pushersService().testPush( currentSession.pushersService().testPush(
stringProvider.getString(R.string.pusher_http_url), UnifiedPushHelper.getPushGateway(context),
stringProvider.getString(R.string.pusher_app_id), stringProvider.getString(R.string.pusher_app_id),
pushKey, UnifiedPushHelper.getEndpointOrToken(context) ?: "",
TEST_EVENT_ID TEST_EVENT_ID
) )
} }
@ -50,19 +51,38 @@ class PushersManager @Inject constructor(
return currentSession.pushersService().enqueueAddHttpPusher(createHttpPusher(pushKey)) 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) { suspend fun registerPusherWithFcmKey(pushKey: String) {
val currentSession = activeSessionHolder.getActiveSession() val currentSession = activeSessionHolder.getActiveSession()
currentSession.pushersService().addHttpPusher(createHttpPusher(pushKey)) 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, pushKey,
stringProvider.getString(R.string.pusher_app_id), stringProvider.getString(R.string.pusher_app_id),
profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()), profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()),
localeProvider.current().language, localeProvider.current().language,
appNameProvider.getAppName(), appNameProvider.getAppName(),
activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE", activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url), gateway,
append = false, append = false,
withEventIdOnly = true withEventIdOnly = true
) )

View file

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

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,27 +14,31 @@
* limitations under the License. * 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.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.widget.Toast
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.firebase.messaging.FirebaseMessagingService import com.squareup.moshi.Json
import com.google.firebase.messaging.RemoteMessage import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.network.WifiDetector 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.NotifiableEventResolver
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils 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.VectorDataStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch 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.logger.LoggerTag
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom 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 timber.log.Timber
import javax.inject.Inject 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) private val loggerTag = LoggerTag("Push", LoggerTag.SYNC)
/** /**
* Class extending FirebaseMessagingService. * Hilt injection happen at super.onReceive().
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class VectorFirebaseMessagingService : FirebaseMessagingService() { class VectorMessagingReceiver : MessagingReceiver() {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var notifiableEventResolver: NotifiableEventResolver @Inject lateinit var notifiableEventResolver: NotifiableEventResolver
@Inject lateinit var pusherManager: PushersManager @Inject lateinit var pusherManager: PushersManager
@ -74,21 +95,38 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* Called when message is received. * Called when message is received.
* *
* @param message the message * @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) { 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 { runBlocking {
vectorDataStore.incrementPushCounter() 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 // Diagnostic Push
if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) { if (notification.eventId == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION) val intent = Intent(NotificationUtils.PUSH_ACTION)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent) LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
return return
} }
@ -102,7 +140,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// we are in foreground, let the sync do the things? // we are in foreground, let the sync do the things?
Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore") Timber.tag(loggerTag.value).d("PUSH received in a foreground state, ignore")
} else { } 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 * when the InstanceID token is initially generated, so this is where
* you retrieve the token. * you retrieve the token.
*/ */
override fun onNewToken(refreshedToken: String) { override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated") Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint")
FcmHelper.storeFcmToken(this, refreshedToken)
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { 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)
} }
/** override fun onRegistrationFailed(context: Context, instance: String) {
* Called when the FCM server deletes pending messages. This may be due to: Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
* - 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) override fun onUnregistered(context: Context, instance: String) {
* sent a message to the app on that device. Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
* val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY
* It is recommended that the app do a full sync with the app server after receiving this call. vectorPreferences.setFdroidSyncBackgroundMode(mode)
*/ runBlocking {
override fun onDeletedMessages() { try {
Timber.tag(loggerTag.value).v("## onDeletedMessages()") pusherManager.unregisterPusher(UnifiedPushHelper.getEndpointOrToken(context) ?: "")
} catch (e: Exception) {
Timber.tag(loggerTag.value).d("Probably unregistering a non existant pusher")
}
}
} }
/** /**
* Internal receive method * Internal receive method
* *
* @param data Data map containing message data as key/value pairs. * @param notification Notification containing message data.
* For Set of keys use data.keySet().
*/ */
private fun onMessageReceivedInternal(data: Map<String, String>) { private fun onMessageReceivedInternal(context: Context, notification: Notification) {
try { try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $data") Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $notification")
} else { } else {
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")
} }
// update the badge counter
BadgeProxy.updateBadgeCount(context, notification.unread)
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
if (session == null) { if (session == null) {
Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") Timber.tag(loggerTag.value).w("## Can't sync from push, no current session")
} else { } else {
val eventId = data["event_id"] if (isEventAlreadyKnown(notification.eventId, notification.roomId)) {
val roomId = data["room_id"]
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.tag(loggerTag.value).d("Ignoring push, event already known") Timber.tag(loggerTag.value).d("Ignoring push, event already known")
} else { } else {
// Try to get the Event content faster // Try to get the Event content faster
Timber.tag(loggerTag.value).d("Requesting event in fast lane") 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") Timber.tag(loggerTag.value).d("Requesting background sync")
session.syncService().requireBackgroundSync() session.syncService().requireBackgroundSync()

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.CallEnded 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.utils.EglUtils
import im.vector.app.features.call.vectorCallService import im.vector.app.features.call.vectorCallService
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -272,7 +272,7 @@ class WebRtcCallManager @Inject constructor(
audioManager.setMode(CallAudioManager.Mode.DEFAULT) audioManager.setMode(CallAudioManager.Mode.DEFAULT)
// did we start background sync? so we should stop it // did we start background sync? so we should stop it
if (isInBackground) { if (isInBackground) {
if (FcmHelper.isPushSupported()) { if (!UnifiedPushHelper.isBackgroundSync(context)) {
currentSession?.syncService()?.stopAnyBackgroundSync() currentSession?.syncService()?.stopAnyBackgroundSync()
} else { } else {
// for fdroid we should not stop, it should continue syncing // 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 // 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 // accepted on an other session this device will continue ringing
if (isInBackground) { if (isInBackground) {
if (FcmHelper.isPushSupported()) { if (!UnifiedPushHelper.isBackgroundSync(context)) {
// only for push version as fdroid version is already doing it? // only for push version as fdroid version is already doing it?
currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0)
} else { } 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.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
@ -187,7 +188,15 @@ class HomeActivity :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = MobileScreen.ScreenName.Home analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) 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) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
views.drawerLayout.addDrawerListener(drawerListener) views.drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) { if (isFirstCreation()) {

View file

@ -38,6 +38,7 @@ import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.pushers.PushersManager 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.services.GuardServiceStarter
import im.vector.app.core.utils.combineLatest import im.vector.app.core.utils.combineLatest
import im.vector.app.core.utils.isIgnoringBatteryOptimizations 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.VectorPreferences
import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsBaseFragment
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull 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.pushers.Pusher
import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.RuleIds
import org.matrix.android.sdk.api.session.pushrules.RuleKind import org.matrix.android.sdk.api.session.pushrules.RuleKind
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// Referenced in vector_settings_preferences_root.xml // Referenced in vector_settings_preferences_root.xml
@ -97,16 +98,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
if (isChecked) { updateEnabledForDevice(isChecked)
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.registerPusherWithFcmKey(it)
}
} else {
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.unregisterPusher(it)
session.pushersService().refreshPushers()
}
}
} }
} }
@ -222,7 +214,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
} }
findPreference<VectorPreferenceCategory>(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { findPreference<VectorPreferenceCategory>(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let {
it.isVisible = !FcmHelper.isPushSupported() it.isVisible = UnifiedPushHelper.isBackgroundSync(requireContext())
} }
val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled() val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled()
@ -331,7 +323,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
private fun refreshPref() { private fun refreshPref() {
// This pref may have change from troubleshoot pref fragment // This pref may have change from troubleshoot pref fragment
if (!FcmHelper.isPushSupported()) { if (UnifiedPushHelper.isBackgroundSync(requireContext())) {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY)
?.isChecked = vectorPreferences.autoStartOnBoot() ?.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?) { private fun updateEnabledForAccount(preference: Preference?) {
val pushRuleService = session.pushRuleService() val pushRuleService = session.pushRuleService()
val switchPref = preference as SwitchPreference val switchPref = preference as SwitchPreference

View file

@ -3063,4 +3063,8 @@
<!-- Screen sharing --> <!-- Screen sharing -->
<string name="screen_sharing_notification_title">${app_name} Screen Sharing</string> <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="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_getdistributors_dialog_fcm_fallback">Google Services</string>
<string name="unifiedpush_getdistributors_dialog_background_sync">Background synchronization</string>
</resources> </resources>