Merge pull request #4314 from vector-im/feature/adm/fdroid-notification-reliability

F-Droid variant permanent notification to fix missing notifications
This commit is contained in:
Benoit Marty 2021-11-04 12:28:20 +01:00 committed by GitHub
commit 6f58cbd6c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 226 additions and 18 deletions

1
changelog.d/4298.bugfix Normal file
View file

@ -0,0 +1 @@
Fixing missing F-Droid notifications when in background due to background syncs not triggering

View file

@ -120,9 +120,11 @@ interface Session :
fun requireBackgroundSync() fun requireBackgroundSync()
/** /**
* Launches infinite periodic background syncs * Launches infinite self rescheduling background syncs via the WorkManager
* This does not work in doze mode :/ *
* If battery optimization is on it can work in app standby but that's all :/ * While dozing, syncs will only occur during maintenance windows
* For reliability it's recommended to also start a long running foreground service
* along with disabling battery optimizations
*/ */
fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long)

View file

@ -105,9 +105,8 @@ abstract class SyncService : Service() {
} }
} }
} }
// Attempt to continue scheduling syncs after killed service is restarted
// It's ok to be not sticky because we will explicitly start it again on the next alarm? return START_REDELIVER_INTENT
return START_NOT_STICKY
} }
override fun onDestroy() { override fun onDestroy() {

View file

@ -121,9 +121,9 @@ internal class SyncWorker(context: Context,
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS) .setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
.build() .build()
// Avoid risking multiple chains of syncs by replacing the existing chain
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
} }
fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) {

View file

@ -29,7 +29,7 @@ import javax.inject.Inject
@MatrixScope @MatrixScope
internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver { internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
var isInBackground: Boolean = false var isInBackground: Boolean = true
private set private set
private private

View file

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.app"> package="im.vector.app">
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- <!--
Required for long polling account synchronisation in background. Required for long polling account synchronisation in background.
If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work
@ -24,6 +26,11 @@
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<service
android:name=".fdroid.service.GuardService"
android:exported="false"
tools:ignore="Instantiatable" />
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,40 @@
/*
* 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.di
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.fdroid.service.FDroidGuardServiceStarter
import im.vector.app.features.settings.VectorPreferences
@Module
@InstallIn(SingletonComponent::class)
abstract class FlavorModule {
companion object {
@Provides
@JvmStatic
fun provideGuardServiceStarter(preferences: VectorPreferences, appContext: Context): GuardServiceStarter {
return FDroidGuardServiceStarter(preferences, appContext)
}
}
}

View file

@ -27,9 +27,8 @@ object BackgroundSyncStarter {
fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
if (vectorPreferences.areNotificationEnabledForDevice()) { if (vectorPreferences.areNotificationEnabledForDevice()) {
val activeSession = activeSessionHolder.getSafeActiveSession() ?: return val activeSession = activeSessionHolder.getSafeActiveSession() ?: return
when (vectorPreferences.getFdroidSyncBackgroundMode()) { when (vectorPreferences.getFdroidSyncBackgroundMode()) {
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> { BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> {
// we rely on periodic worker // we rely on periodic worker
Timber.i("## Sync: Work scheduled to periodically sync in ${vectorPreferences.backgroundSyncDelay()}s") Timber.i("## Sync: Work scheduled to periodically sync in ${vectorPreferences.backgroundSyncDelay()}s")
activeSession.startAutomaticBackgroundSync( activeSession.startAutomaticBackgroundSync(
@ -42,7 +41,7 @@ object BackgroundSyncStarter {
AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay()) AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay())
Timber.i("## Sync: Alarm scheduled to start syncing") Timber.i("## Sync: Alarm scheduled to start syncing")
} }
BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> {
// we do nothing // we do nothing
Timber.i("## Sync: background sync is disabled") Timber.i("## Sync: background sync is disabled")
} }

View file

@ -0,0 +1,48 @@
/*
* 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.fdroid.service
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber
import javax.inject.Inject
class FDroidGuardServiceStarter @Inject constructor(
private val preferences: VectorPreferences,
private val appContext: Context
) : GuardServiceStarter {
override fun start() {
if (preferences.isBackgroundSyncEnabled()) {
try {
Timber.i("## Sync: starting GuardService")
val intent = Intent(appContext, GuardService::class.java)
ContextCompat.startForegroundService(appContext, intent)
} catch (ex: Throwable) {
Timber.e("## Sync: ERROR starting GuardService")
}
}
}
override fun stop() {
val intent = Intent(appContext, GuardService::class.java)
appContext.stopService(intent)
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.fdroid.service
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.services.VectorService
import im.vector.app.features.notifications.NotificationUtils
import javax.inject.Inject
/**
* This no-op foreground service acts as a deterrent to the system eagerly killing the app process.
*
* Keeping the app process alive avoids some OEMs ignoring scheduled WorkManager and AlarmManager tasks
* when the app is not in the foreground.
*/
@AndroidEntryPoint
class GuardService : VectorService() {
@Inject lateinit var notificationUtils: NotificationUtils
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notificationSubtitleRes = R.string.notification_listening_for_notifications
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
return START_STICKY
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.core.services.GuardServiceStarter
@InstallIn(SingletonComponent::class)
@Module
abstract class FlavorModule {
companion object {
@Provides
@JvmStatic
fun provideGuardServiceStarter(): GuardServiceStarter {
return object : GuardServiceStarter {}
}
}
}

View file

@ -108,6 +108,6 @@ object FcmHelper {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
// TODO FCM fallback // No op
} }
} }

View file

@ -18,6 +18,7 @@ package im.vector.app.core.di
import arrow.core.Option import arrow.core.Option
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
@ -36,7 +37,8 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val pushRuleTriggerListener: PushRuleTriggerListener, private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener, private val sessionListener: SessionListener,
private val imageManager: ImageManager private val imageManager: ImageManager,
private val guardServiceStarter: GuardServiceStarter
) { ) {
private var activeSession: AtomicReference<Session?> = AtomicReference() private var activeSession: AtomicReference<Session?> = AtomicReference()
@ -52,6 +54,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore
pushRuleTriggerListener.startWithSession(session) pushRuleTriggerListener.startWithSession(session)
session.callSignalingService().addCallListener(callManager) session.callSignalingService().addCallListener(callManager)
imageManager.onSessionStarted(session) imageManager.onSessionStarted(session)
guardServiceStarter.start()
} }
fun clearActiveSession() { fun clearActiveSession() {

View file

@ -0,0 +1,22 @@
/*
* 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.services
interface GuardServiceStarter {
fun start() {}
fun stop() {}
}

View file

@ -83,7 +83,7 @@ class VectorSyncService : SyncService() {
val notificationSubtitleRes = if (isInitialSync) { val notificationSubtitleRes = if (isInitialSync) {
R.string.notification_initial_sync R.string.notification_initial_sync
} else { } else {
R.string.notification_listening_for_events R.string.notification_listening_for_notifications
} }
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)

View file

@ -37,6 +37,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.services.GuardServiceStarter
import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
@ -61,7 +62,8 @@ import javax.inject.Inject
class VectorSettingsNotificationPreferenceFragment @Inject constructor( class VectorSettingsNotificationPreferenceFragment @Inject constructor(
private val pushManager: PushersManager, private val pushManager: PushersManager,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
private val guardServiceStarter: GuardServiceStarter
) : VectorSettingsBaseFragment(), ) : VectorSettingsBaseFragment(),
BackgroundSyncModeChooserDialog.InteractionListener { BackgroundSyncModeChooserDialog.InteractionListener {
@ -216,14 +218,19 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
it.isVisible = !FcmHelper.isPushSupported() it.isVisible = !FcmHelper.isPushSupported()
} }
val backgroundSyncEnabled = vectorPreferences.isBackgroundSyncEnabled()
findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let { findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let {
it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() it.isEnabled = backgroundSyncEnabled
it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut()) it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut())
} }
findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY)?.let { findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY)?.let {
it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() it.isEnabled = backgroundSyncEnabled
it.summary = secondsToText(vectorPreferences.backgroundSyncDelay()) it.summary = secondsToText(vectorPreferences.backgroundSyncDelay())
} }
when {
backgroundSyncEnabled -> guardServiceStarter.start()
else -> guardServiceStarter.stop()
}
} }
/** /**

View file

@ -322,6 +322,7 @@
<string name="notification_sync_init">Initializing service</string> <string name="notification_sync_init">Initializing service</string>
<string name="notification_sync_in_progress">Synchronising…</string> <string name="notification_sync_in_progress">Synchronising…</string>
<string name="notification_listening_for_events">Listening for events</string> <string name="notification_listening_for_events">Listening for events</string>
<string name="notification_listening_for_notifications">Listening for notifications</string>
<string name="notification_noisy_notifications">Noisy notifications</string> <string name="notification_noisy_notifications">Noisy notifications</string>
<string name="notification_silent_notifications">Silent notifications</string> <string name="notification_silent_notifications">Silent notifications</string>