unifiedpush notification

This commit is contained in:
user 2021-06-01 20:29:44 +02:00
parent d2fd652fb5
commit a3a946fb75
24 changed files with 403 additions and 342 deletions

View File

@ -44,6 +44,8 @@ allprojects {
includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.vector-im'
// UnifiedPush
includeGroupByRegex 'com\\.github\\.UnifiedPush'
// Chat effects
includeGroupByRegex 'com\\.github\\.jetradarmobile'

View File

@ -291,6 +291,10 @@ android {
buildFeatures {
viewBinding true
}
packagingOptions {
exclude 'META-INF/lib_release.kotlin_module'
}
}
dependencies {
@ -429,12 +433,9 @@ dependencies {
implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
// gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:22.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'
}
// Unifiedpush
implementation 'com.github.UnifiedPush:android-connector:1.1.3'
gplayImplementation 'com.github.UnifiedPush:android-embedded_fcm_distributor:1.0.2'
// OSS License, gplay flavor only
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.app">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--
Required for long polling account synchronisation in background.
If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application>
<receiver android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".fdroid.receiver.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@ -17,56 +17,13 @@
package im.vector.app.push.fcm
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.fdroid.BackgroundSyncStarter
import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver
import im.vector.app.features.settings.VectorPreferences
/**
* This class has an alter ego in the gplay variant.
*/
object FcmHelper {
fun isPushSupported(): Boolean = false
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
fun getFcmToken(context: Context): 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?) {
// No op
}
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// No op
}
fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) {
// try to stop all regardless of background mode
activeSessionHolder.getSafeActiveSession()?.stopAnyBackgroundSync()
AlarmSyncBroadcastReceiver.cancelAlarm(context)
}
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder)
fun isPlayServicesAvailable(context: Context): Boolean {
return false
}
}

View File

@ -3,18 +3,12 @@
package="im.vector.app">
<application>
<!-- Firebase components -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<service android:name=".gplay.push.fcm.VectorFirebaseMessagingService">
<receiver android:enabled="true" android:name=".push.fcm.EmbeddedDistrib" 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>
</manifest>
</manifest>

View File

@ -18,7 +18,6 @@ package im.vector.app.gplay.features.settings.troubleshoot
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.messaging.FirebaseMessaging
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.startAddGoogleAccountIntent
@ -28,13 +27,18 @@ import timber.log.Timber
import javax.inject.Inject
/*
* Test that app can successfully retrieve a token via firebase
* Test that app can successfully retrieve a new endpoint
*/
class TestFirebaseToken @Inject constructor(private val context: AppCompatActivity,
class TestNewEndpoint @Inject constructor(private val context: AppCompatActivity,
private val stringProvider: StringProvider) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) {
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
status = TestStatus.RUNNING
description = "bypassed"
status = TestStatus.SUCCESS
/**
* TODO
*/
/*status = TestStatus.RUNNING
try {
FirebaseMessaging.getInstance().token
.addOnCompleteListener(context) { task ->
@ -74,6 +78,6 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi
} catch (e: Throwable) {
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, e.localizedMessage)
status = TestStatus.FAILED
}
}*/
}
}

View File

@ -25,7 +25,7 @@ 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 im.vector.app.core.pushers.UPHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -48,12 +48,12 @@ class TestPushFromPushGateway @Inject constructor(private val context: AppCompat
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
pushReceived = false
val fcmToken = FcmHelper.getFcmToken(context) ?: run {
UPHelper.getUpEndpoint(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

View File

@ -27,7 +27,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import im.vector.app.push.fcm.FcmHelper
import im.vector.app.core.pushers.UPHelper
import javax.inject.Inject
/**
@ -41,7 +41,11 @@ class TestTokenRegistration @Inject constructor(private val context: AppCompatAc
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
// Check if we have a registered pusher for this token
val fcmToken = FcmHelper.getFcmToken(context) ?: run {
val pushToken = UPHelper.getUpEndpoint(context) ?: run {
status = TestStatus.FAILED
return
}
val pushGateway = UPHelper.getPushGateway(context) ?: run {
status = TestStatus.FAILED
return
}
@ -50,14 +54,14 @@ class TestTokenRegistration @Inject constructor(private val context: AppCompatAc
return
}
val pushers = session.getPushers().filter {
it.pushKey == fcmToken && it.state == PusherState.REGISTERED
it.pushKey == pushToken && it.state == PusherState.REGISTERED
}
if (pushers.isEmpty()) {
description = stringProvider.getString(R.string.settings_troubleshoot_test_token_registration_failed,
stringProvider.getString(R.string.sas_error_unknown))
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) {
override fun doFix() {
val workId = pushersManager.registerPusherWithFcmKey(fcmToken)
val workId = pushersManager.registerPusher(pushToken, pushGateway)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
if (workInfo != null) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {

View File

@ -0,0 +1,31 @@
package im.vector.app.push.fcm
import android.content.Context
import im.vector.app.R
import org.unifiedpush.android.embedded_fcm_distributor.GetEndpointHandler
import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver
/*
* Copyright (c) 2021 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.
*/
val handlerFCM = object: GetEndpointHandler {
override fun getEndpoint(context: Context?, token: String, instance: String): String {
// This returns the endpoint of your FCM Rewrite-Proxy
return "${context!!.getString(R.string.pusher_http_url)}/FCM?instance=$instance&token=$token"
}
}
class EmbeddedDistrib: EmbeddedDistributorReceiver(handlerFCM)

View File

@ -15,99 +15,21 @@
*/
package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context
import android.widget.Toast
import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.messaging.FirebaseMessaging
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.features.settings.VectorPreferences
import timber.log.Timber
/**
* 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"
fun isPushSupported(): Boolean = true
/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
* Check the device to make sure it has the Google Play Services APK.
*/
fun getFcmToken(context: Context): String? {
return DefaultSharedPreferences.getInstance(context).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 {
putString(PREFS_KEY_FCM_TOKEN, token)
}
}
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
storeFcmToken(activity, token)
if (registerPusher) {
pushersManager.registerPusherWithFcmKey(token)
}
}
.addOnFailureListener { e ->
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
}
} catch (e: Throwable) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
}
} else {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Timber.e("No valid Google Play Services found. Cannot use FCM.")
}
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* 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 isPlayServicesAvailable(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) {
// No op
}
@Suppress("UNUSED_PARAMETER")
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
// TODO FCM fallback
}
}

View File

@ -22,7 +22,7 @@ import im.vector.app.features.settings.troubleshoot.TestDeviceSettings
import im.vector.app.features.settings.troubleshoot.TestNotification
import im.vector.app.features.settings.troubleshoot.TestPushRulesSettings
import im.vector.app.features.settings.troubleshoot.TestSystemSettings
import im.vector.app.gplay.features.settings.troubleshoot.TestFirebaseToken
import im.vector.app.gplay.features.settings.troubleshoot.TestNewEndpoint
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
@ -34,7 +34,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(
private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestPushRulesSettings,
private val testPlayServices: TestPlayServices,
private val testFirebaseToken: TestFirebaseToken,
private val testNewEndpoint: TestNewEndpoint,
private val testTokenRegistration: TestTokenRegistration,
private val testPushFromPushGateway: TestPushFromPushGateway,
private val testNotification: TestNotification
@ -47,7 +47,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(
mgr.addTest(testDeviceSettings)
mgr.addTest(testBingRulesSettings)
mgr.addTest(testPlayServices)
mgr.addTest(testFirebaseToken)
mgr.addTest(testNewEndpoint)
mgr.addTest(testTokenRegistration)
mgr.addTest(testPushFromPushGateway)
mgr.addTest(testNotification)

View File

@ -39,6 +39,14 @@
android:name="android.permission.WRITE_CALENDAR"
tools:node="remove" />
<!-- if the user don't have FCM or any unifiedpush distributor -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--
Required for long polling account synchronisation in background.
If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Jitsi SDK is now API23+ -->
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg" />
@ -382,6 +390,30 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/sdk_provider_paths" />
</provider>
<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>
<!-- For the fallback with sync loop if the user don't have FCM or a UnifiedPush Distributor -->
<receiver android:name=".core.receiver.OnApplicationUpgradeOrRebootReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".core.receiver.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

@ -58,7 +58,7 @@ import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.version.VersionProvider
import im.vector.app.push.fcm.FcmHelper
import im.vector.app.core.pushers.StateHelper
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
@ -169,7 +169,7 @@ class VectorApplication :
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
Timber.i("App entered foreground")
FcmHelper.onEnterForeground(appContext, activeSessionHolder)
StateHelper.onEnterForeground(appContext, activeSessionHolder)
activeSessionHolder.getSafeActiveSession()?.also {
it.stopAnyBackgroundSync()
}
@ -179,7 +179,7 @@ class VectorApplication :
fun entersBackground() {
Timber.i("App entered background") // call persistInfo
notificationDrawerManager.persistInfo()
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
StateHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
}
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)

View File

@ -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
@ -33,29 +34,29 @@ 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.testPush(
stringProvider.getString(R.string.pusher_http_url),
UPHelper.getPushGateway(context)!!,
stringProvider.getString(R.string.pusher_app_id),
pushKey,
UPHelper.getUpEndpoint(context)!!,
TEST_EVENT_ID
)
}
fun registerPusherWithFcmKey(pushKey: String): UUID {
fun registerPusher(pushKey: String, gateway: String): UUID {
val currentSession = activeSessionHolder.getActiveSession()
val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode())
return currentSession.addHttpPusher(
pushKey,
pushKey, // this is the UnifiedPush endpoint
stringProvider.getString(R.string.pusher_app_id),
profileTag,
localeProvider.current().language,
appNameProvider.getAppName(),
currentSession.sessionParams.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url),
gateway,
append = false,
withEventIdOnly = true
)

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2021 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 im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.receiver.AlarmSyncBroadcastReceiver
import im.vector.app.core.receiver.BackgroundSyncStarter
import im.vector.app.features.settings.VectorPreferences
object StateHelper {
fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) {
// try to stop all regardless of background mode
activeSessionHolder.getSafeActiveSession()?.stopAnyBackgroundSync()
AlarmSyncBroadcastReceiver.cancelAlarm(context)
}
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder)
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2021 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.appcompat.app.AlertDialog
import androidx.core.content.edit
import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.push.fcm.FcmHelper
import org.unifiedpush.android.connector.Registration
import timber.log.Timber
import java.net.URI
/**
* This class store the UnifiedPush Endpoint in SharedPrefs and ensure this token is retrieved.
* It has an alter ego in the fdroid variant.
*/
object UPHelper {
private const val PREFS_UP_ENDPOINT = "UP_ENDPOINT"
private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY"
/**
* Retrieves the UnifiedPush Endpoint.
*
* @return the UnifiedPush Endpoint or null if not received
*/
fun getUpEndpoint(context: Context): String? {
return DefaultSharedPreferences.getInstance(context).getString(PREFS_UP_ENDPOINT, 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_UP_ENDPOINT, 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 registerUnifiedPush(context: Context) {
val up = Registration()
if (up.getDistributor(context).isNotEmpty()) {
up.registerApp(context)
return
}
val distributors = up.getDistributors(context).toMutableList()
/**
* Check if it is the gplay flavour AND GServices are not available
*/
if (!FcmHelper.isPlayServicesAvailable(context)) {
distributors.remove(context.packageName)
}
when (distributors.size) {
0 -> {
/**
* TODO: fallback with sync service : automatic ?
*/
}
1 -> {
up.saveDistributor(context, distributors.first())
up.registerApp(context)
}
else -> {
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder.setTitle("Choose a distributor")
val distributorsArray = distributors.toTypedArray()
builder.setItems(distributorsArray) { _, which ->
val distributor = distributorsArray[which]
up.saveDistributor(context, distributor)
Timber.i("Saving distributor: $distributor")
up.registerApp(context)
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
}
fun unregister(context: Context) {
val up = Registration()
up.unregisterApp(context)
}
fun customOrDefaultGateway(context: Context, endpoint: String?): String {
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 hasEndpoint(context: Context): Boolean {
getUpEndpoint(context)?.let {
return true
}
return false
}
}

View File

@ -17,43 +17,41 @@
* 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 im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.vectorComponent
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.NotifiableMessageEvent
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.notifications.SimpleNotifiableEvent
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.json.JSONException
import org.json.JSONObject
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.unifiedpush.android.connector.MessagingReceiver
import org.unifiedpush.android.connector.MessagingReceiverHandler
import timber.log.Timber
/**
* Class extending FirebaseMessagingService.
* Unifiedpush handler.
*/
class VectorFirebaseMessagingService : FirebaseMessagingService() {
val upHandler = object: MessagingReceiverHandler {
private lateinit var notificationDrawerManager: NotificationDrawerManager
private lateinit var notifiableEventResolver: NotifiableEventResolver
@ -69,9 +67,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Handler(Looper.getMainLooper())
}
override fun onCreate() {
super.onCreate()
with(vectorComponent()) {
fun initVar(context: Context) {
with(context.vectorComponent()) {
notificationDrawerManager = notificationDrawerManager()
notifiableEventResolver = notifiableEventResolver()
pusherManager = pusherManager()
@ -85,17 +82,42 @@ 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: String, instance: String) {
initVar(context!!)
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.d("## onMessageReceived() %s", message.data.toString())
Timber.d("## onMessageReceived() %s", message)
}
Timber.d("## onMessage() received")
lateinit var data: JSONObject
lateinit var notification: JSONObject
try {
data = JSONObject(message)
notification = data.getJSONObject("notification")
} catch (e: JSONException) {
Timber.e(e)
return
}
val eventId: String = try {
notification.getString("event_id")
} catch (e: JSONException) {
Timber.i("No event_id on notification")
notification.put("event_id", "")
""
}
try {
notification.getString("room_id")
} catch (e: JSONException) {
Timber.i("No room_id on notification")
notification.put("room_id", "")
}
Timber.d("## onMessageReceived() from FCM with priority %s", message.priority)
// Diagnostic Push
if (message.data["event_id"] == PushersManager.TEST_EVENT_ID) {
if (eventId == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
return
}
@ -109,7 +131,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// we are in foreground, let the sync do the things?
Timber.d("PUSH received in a foreground state, ignore")
} else {
onMessageReceivedInternal(message.data)
onMessageReceivedInternal(context, notification)
}
}
}
@ -120,52 +142,60 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
* when the InstanceID token is initially generated, so this is where
* you retrieve the token.
*/
override fun onNewToken(refreshedToken: String) {
Timber.i("onNewToken: FCM Token has been updated")
FcmHelper.storeFcmToken(this, refreshedToken)
override fun onNewEndpoint(context: Context?, endpoint: String, instance: String) {
initVar(context!!)
Timber.i("onNewEndpoint: adding $endpoint")
UPHelper.storeUpEndpoint(context, endpoint)
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
pusherManager.registerPusherWithFcmKey(refreshedToken)
val gateway = UPHelper.customOrDefaultGateway(context, endpoint)
UPHelper.storePushGateway(context, gateway)
UPHelper.storeUpEndpoint(context, endpoint)
pusherManager.registerPusher(endpoint, gateway)
}
}
/**
* 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.v("## onDeletedMessages()")
override fun onRegistrationFailed(context: Context?, instance: String) {
Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show()
}
override fun onRegistrationRefused(context: Context?, instance: String) {
Toast.makeText(context, "Push service registration refused by server", Toast.LENGTH_LONG).show()
}
override fun onUnregistered(context: Context?, instance: String) {
Timber.d("Unifiedpush: Unregistered")
initVar(context!!)
runBlocking {
try {
pusherManager.unregisterPusher(UPHelper.getUpEndpoint(context)!!)
} catch (e: Exception) {
Timber.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 data Data json containing message data.
*/
private fun onMessageReceivedInternal(data: Map<String, String>) {
private fun onMessageReceivedInternal(context: Context, data: JSONObject) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.d("## onMessageReceivedInternal() : $data")
} else {
Timber.d("## onMessageReceivedInternal() : $data")
}
// update the badge counter
val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
val unreadCount = data.getJSONObject("counts").getInt("unread")
BadgeProxy.updateBadgeCount(context, unreadCount)
val session = activeSessionHolder.getSafeActiveSession()
if (session == null) {
Timber.w("## Can't sync from push, no current session")
} else {
val eventId = data["event_id"]
val roomId = data["room_id"]
val eventId = data.getString("event_id")
val roomId = data.getString("room_id")
if (isEventAlreadyKnown(eventId, roomId)) {
Timber.d("Ignoring push, event already known")
@ -227,87 +257,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
}
return false
}
private fun handleNotificationWithoutSyncingMode(data: Map<String, String>, session: Session?) {
if (session == null) {
Timber.e("## handleNotificationWithoutSyncingMode cannot find session")
return
}
// The Matrix event ID of the event being notified about.
// This is required if the notification is about a particular Matrix event.
// It may be omitted for notifications that only contain updated badge counts.
// This ID can and should be used to detect duplicate notification requests.
val eventId = data["event_id"] ?: return // Just ignore
val eventType = data["type"]
if (eventType == null) {
// Just add a generic unknown event
val simpleNotifiableEvent = SimpleNotifiableEvent(
session.myUserId,
eventId,
null,
true, // It's an issue in this case, all event will bing even if expected to be silent.
title = getString(R.string.notification_unknown_new_event),
description = "",
type = null,
timestamp = System.currentTimeMillis(),
soundName = Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT,
isPushGatewayEvent = true
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
} else {
val event = parseEvent(data) ?: return
val notifiableEvent = notifiableEventResolver.resolveEvent(event, session)
if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event $eventId")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> $event")
}
} else {
if (notifiableEvent is NotifiableMessageEvent) {
if (notifiableEvent.senderName.isNullOrEmpty()) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
}
if (notifiableEvent.roomName.isNullOrEmpty()) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
}
}
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.myUserId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
}
}
}
private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
var roomName: String? = data["room_name"]
val roomId = data["room_id"]
if (null == roomName && null != roomId) {
// Try to get the room name from our store
roomName = session?.getRoom(roomId)?.roomSummary()?.displayName
}
return roomName
}
/**
* Try to create an event from the FCM data
*
* @param data the FCM data
* @return the event or null if required data are missing
*/
private fun parseEvent(data: Map<String, String>?): Event? {
return Event(
eventId = data?.get("event_id") ?: return null,
senderId = data["sender"],
roomId = data["room_id"] ?: return null,
type = data["type"] ?: return null,
originServerTs = System.currentTimeMillis()
)
}
}
class VectorMessagingReceiver : MessagingReceiver(upHandler)

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.app.fdroid.receiver
package im.vector.app.core.receiver
import android.app.AlarmManager
import android.app.PendingIntent

View File

@ -14,11 +14,10 @@
* limitations under the License.
*/
package im.vector.app.fdroid
package im.vector.app.core.receiver
import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder
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

View File

@ -15,14 +15,13 @@
* limitations under the License.
*/
package im.vector.app.fdroid.receiver
package im.vector.app.core.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.extensions.vectorComponent
import im.vector.app.fdroid.BackgroundSyncStarter
import timber.log.Timber
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {

View File

@ -28,7 +28,7 @@ import im.vector.app.features.call.lookup.CallProtocolsChecker
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.push.fcm.FcmHelper
import im.vector.app.core.pushers.UPHelper
import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -243,7 +243,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 (UPHelper.hasEndpoint(context)) {
currentSession?.stopAnyBackgroundSync()
} else {
// for fdroid we should not stop, it should continue syncing
@ -348,7 +348,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 (UPHelper.hasEndpoint(context)) {
// only for push version as fdroid version is already doing it?
currentSession?.startAutomaticBackgroundSync(30, 0)
} else {

View File

@ -67,7 +67,7 @@ import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import im.vector.app.push.fcm.FcmHelper
import im.vector.app.core.pushers.UPHelper
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
@ -164,7 +164,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
UPHelper.registerUnifiedPush(this)
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
views.drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) {

View File

@ -37,11 +37,12 @@ import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.push.fcm.FcmHelper
import im.vector.app.core.pushers.UPHelper
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind
import timber.log.Timber
import javax.inject.Inject
// Referenced in vector_settings_preferences_root.xml
@ -145,7 +146,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
}
findPreference<VectorPreferenceCategory>(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let {
it.isVisible = !FcmHelper.isPushSupported()
it.isVisible = !UPHelper.hasEndpoint(requireContext())
}
findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let {
@ -249,7 +250,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
private fun refreshPref() {
// This pref may have change from troubleshoot pref fragment
if (!FcmHelper.isPushSupported()) {
if (!UPHelper.hasEndpoint(requireContext())) {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY)
?.isChecked = vectorPreferences.autoStartOnBoot()
}
@ -289,13 +290,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
private fun updateEnabledForDevice(preference: Preference?) {
val switchPref = preference as SwitchPreference
if (switchPref.isChecked) {
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.registerPusherWithFcmKey(it)
}
UPHelper.registerUnifiedPush(requireContext())
} else {
FcmHelper.getFcmToken(requireContext())?.let {
UPHelper.getUpEndpoint(requireContext())?.let {
lifecycleScope.launch {
runCatching { pushManager.unregisterPusher(it) }
runCatching {
try {
pushManager.unregisterPusher(it)
} catch (e: Exception) {
Timber.d("Probably unregistering a non existant pusher")
}
try {
UPHelper.unregister(requireContext())
} catch (e: Exception) {
Timber.d("Probably unregistering to a non-saved distributor")
}
}
.fold(
{ session.refreshPushers() },
{

View File

@ -15,6 +15,8 @@
<!-- Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
<string name="pusher_http_url" translatable="false">https://matrix.org/_matrix/push/v1/notify</string>
<!-- Note: pusher_http_url should have path '/_matrix/push/v1/notify' -->
<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>