diff --git a/CHANGES.md b/CHANGES.md index eada6082b9..00966ef2d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Handle date formatting properly (show time am/pm if needed, display year when needed) + - Improve F-Droid Notification (#2055) Bugfix 🐛: - Clear the notification when the event is read elsewhere (#1822) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index cfddf73363..4dfc24ddae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -110,7 +110,7 @@ interface Session : * This does not work in doze mode :/ * If battery optimization is on it can work in app standby but that's all :/ */ - fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) + fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) fun stopAnyBackgroundSync() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index f8ba625947..004c5afe8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -166,8 +166,8 @@ internal class DefaultSession @Inject constructor( SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) } - override fun startAutomaticBackgroundSync(repeatDelay: Long) { - SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) + override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) { + SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds) } override fun stopAnyBackgroundSync() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 02afd53908..9924d44764 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -32,7 +32,7 @@ import javax.inject.Inject internal interface SyncTask : Task { - data class Params(var timeout: Long = 30_000L) + data class Params(var timeout: Long = 6_000L) } internal class DefaultSyncTask @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt index 20aa409336..485eca6f74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -19,7 +19,14 @@ package org.matrix.android.sdk.internal.session.sync.job import android.app.Service import android.content.Intent import android.os.IBinder +import android.os.PowerManager +import androidx.core.content.getSystemService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.sync.SyncState @@ -28,10 +35,6 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -46,6 +49,11 @@ abstract class SyncService : Service() { private var sessionId: String? = null private var mIsSelfDestroyed: Boolean = false + private var syncTimeoutSeconds: Int = 6 + private var syncDelaySeconds: Int = 60 + private var periodic: Boolean = false + private var preventReschedule: Boolean = false + private var isInitialSync: Boolean = false private lateinit var session: Session private lateinit var syncTask: SyncTask @@ -59,27 +67,60 @@ abstract class SyncService : Service() { private val serviceScope = CoroutineScope(SupervisorJob()) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Timber.i("onStartCommand $intent") - val isInit = initialize(intent) - if (isInit) { - onStart(isInitialSync) - doSyncIfNotAlreadyRunning() - } else { - // We should start and stop as we have to ensure to call Service.startForeground() - onStart(isInitialSync) - stopMe() + Timber.i("## Sync: onStartCommand [$this] $intent with action: ${intent?.action}") + + // We should start we have to ensure we fulfill contract to show notification + // for foreground service (as per design for this service) + // TODO can we check if it's really in foreground + onStart(isInitialSync) + when (intent?.action) { + ACTION_STOP -> { + Timber.i("## Sync: stop command received") + // If it was periodic we ensure that it will not reschedule itself + preventReschedule = true + // we don't want to cancel initial syncs, let it finish + if (!isInitialSync) { + stopMe() + } + } + else -> { + val isInit = initialize(intent) + if (isInit) { + periodic = intent?.getBooleanExtra(EXTRA_PERIODIC, false) ?: false + val onNetworkBack = intent?.getBooleanExtra(EXTRA_NETWORK_BACK_RESTART, false) ?: false + Timber.d("## Sync: command received, periodic: $periodic networkBack: $onNetworkBack") + if (onNetworkBack && !backgroundDetectionObserver.isInBackground) { + // the restart after network occurs while the app is in foreground + // so just stop. It will be restarted when entering background + preventReschedule = true + stopMe() + } else { + // default is syncing + doSyncIfNotAlreadyRunning() + } + } else { + Timber.d("## Sync: Failed to initialize service") + stopMe() + } + } } - // No intent just start the service, an alarm will should call with intent - return START_STICKY + + // It's ok to be not sticky because we will explicitly start it again on the next alarm? + return START_NOT_STICKY } override fun onDestroy() { - Timber.i("## onDestroy() : $this") + Timber.i("## Sync: onDestroy() [$this] periodic:$periodic preventReschedule:$preventReschedule") if (!mIsSelfDestroyed) { - Timber.w("## Destroy by the system : $this") + Timber.d("## Sync: Destroy by the system : $this") } - serviceScope.coroutineContext.cancelChildren() isRunning.set(false) + // Cancelling the context will trigger the catch close the doSync try + serviceScope.coroutineContext.cancelChildren() + if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) { + Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec") + onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds) + } super.onDestroy() } @@ -90,9 +131,15 @@ abstract class SyncService : Service() { private fun doSyncIfNotAlreadyRunning() { if (isRunning.get()) { - Timber.i("Received a start while was already syncing... ignore") + Timber.i("## Sync: Received a start while was already syncing... ignore") } else { isRunning.set(true) + // Acquire a lock to give enough time for the sync :/ + getSystemService()?.run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply { + acquire((syncTimeoutSeconds * 1000L + 10_000L)) + } + } serviceScope.launch(coroutineDispatchers.io) { doSync() } @@ -100,9 +147,10 @@ abstract class SyncService : Service() { } private suspend fun doSync() { - Timber.v("Execute sync request with timeout 0") - val params = SyncTask.Params(TIME_OUT) + Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds") + val params = SyncTask.Params(syncTimeoutSeconds * 1000L) try { + // never do that in foreground, let the syncThread work syncTask.execute(params) // Start sync if we were doing an initial sync and the syncThread is not launched yet if (isInitialSync && session.getSyncState() == SyncState.Idle) { @@ -111,28 +159,34 @@ abstract class SyncService : Service() { } stopMe() } catch (throwable: Throwable) { - Timber.e(throwable) + Timber.e(throwable, "## Sync: sync service did fail ${isRunning.get()}") if (throwable.isTokenError()) { - stopMe() - } else { - Timber.v("Should be rescheduled to avoid wasting resources") - sessionId?.also { - onRescheduleAsked(it, isInitialSync, delay = 10_000L) - } - stopMe() + // no need to retry + preventReschedule = true } + if (throwable is Failure.NetworkConnection) { + // Network is off, no need to reschedule endless alarms :/ + preventReschedule = true + // Instead start a work to restart background sync when network is back + onNetworkError(sessionId ?: "", isInitialSync, syncTimeoutSeconds, syncDelaySeconds) + } + // JobCancellation could be caught here when onDestroy cancels the coroutine context + if (isRunning.get()) stopMe() } } private fun initialize(intent: Intent?): Boolean { if (intent == null) { + Timber.d("## Sync: initialize intent is null") return false } val matrix = Matrix.getInstance(applicationContext) val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false + syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6) + syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60) try { val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) - ?: throw IllegalStateException("You should have a session to make it work") + ?: throw IllegalStateException("## Sync: You should have a session to make it work") session = sessionComponent.session() sessionId = safeSessionId syncTask = sessionComponent.syncTask() @@ -143,14 +197,16 @@ abstract class SyncService : Service() { backgroundDetectionObserver = matrix.backgroundDetectionObserver return true } catch (exception: Exception) { - Timber.e(exception, "An exception occurred during initialisation") + Timber.e(exception, "## Sync: An exception occurred during initialisation") return false } } abstract fun onStart(isInitialSync: Boolean) - abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) + abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) + + abstract fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) override fun onBind(intent: Intent?): IBinder? { return null @@ -158,6 +214,11 @@ abstract class SyncService : Service() { companion object { const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" - private const val TIME_OUT = 0L + const val EXTRA_TIMEOUT_SECONDS = "EXTRA_TIMEOUT_SECONDS" + const val EXTRA_DELAY_SECONDS = "EXTRA_DELAY_SECONDS" + const val EXTRA_PERIODIC = "EXTRA_PERIODIC" + const val EXTRA_NETWORK_BACK_RESTART = "EXTRA_NETWORK_BACK_RESTART" + + const val ACTION_STOP = "ACTION_STOP" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index e702de3573..3e0a29ba72 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -34,7 +34,8 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -private const val DEFAULT_LONG_POOL_TIMEOUT = 0L +private const val DEFAULT_LONG_POOL_TIMEOUT = 6L +private const val DEFAULT_DELAY_TIMEOUT = 30_000L /** * Possible previous worker: None @@ -48,13 +49,15 @@ internal class SyncWorker(context: Context, internal data class Params( override val sessionId: String, val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, - val automaticallyRetry: Boolean = false, + val delay: Long = DEFAULT_DELAY_TIMEOUT, + val periodic: Boolean = false, override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var syncTask: SyncTask @Inject lateinit var taskExecutor: TaskExecutor @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker + @Inject lateinit var workManagerProvider: WorkManagerProvider override suspend fun doWork(): Result { Timber.i("Sync work starting") @@ -67,11 +70,21 @@ internal class SyncWorker(context: Context, return runCatching { doSync(params.timeout) }.fold( - { Result.success() }, + { + Result.success().also { + if (params.periodic) { + // we want to schedule another one after delay + automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay) + } + } + }, { failure -> - if (failure.isTokenError() || !params.automaticallyRetry) { + if (failure.isTokenError()) { Result.failure() } else { + // If the worker was stopped (when going back in foreground), a JobCancellation exception is sent + // but in this case the result is ignored, as the work is considered stopped, + // so don't worry of the retry here for this case Result.retry() } } @@ -79,7 +92,7 @@ internal class SyncWorker(context: Context, } private suspend fun doSync(timeout: Long) { - val taskParams = SyncTask.Params(timeout) + val taskParams = SyncTask.Params(timeout * 1000) syncTask.execute(taskParams) } @@ -87,25 +100,27 @@ internal class SyncWorker(context: Context, private const val BG_SYNC_WORK_NAME = "BG_SYNCP" fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false)) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .setInputData(data) .build() workManagerProvider.workManager - .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) } - fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) { + val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true)) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(data) - .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) + .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) + .setInitialDelay(delayInSeconds, TimeUnit.SECONDS) .build() + workManagerProvider.workManager - .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) } fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index ebe877b301..3ced7de7e2 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -151,7 +151,7 @@ android\.app\.AlertDialog new Gson\(\) ### Use matrixOneTimeWorkRequestBuilder -import androidx.work.OneTimeWorkRequestBuilder===1 +import androidx.work.OneTimeWorkRequestBuilder===2 ### Use TextUtils.formatFileSize Formatter\.formatFileSize===1 @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt -enum class===77 +enum class===78 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml index 55f745ddfa..3a7c107138 100644 --- a/vector/src/fdroid/AndroidManifest.xml +++ b/vector/src/fdroid/AndroidManifest.xml @@ -4,6 +4,11 @@ + + diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt new file mode 100644 index 0000000000..7221e2b065 --- /dev/null +++ b/vector/src/fdroid/java/im/vector/app/fdroid/BackgroundSyncStarter.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 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 + +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 + +object BackgroundSyncStarter { + fun start(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { + if (vectorPreferences.areNotificationEnabledForDevice()) { + val activeSession = activeSessionHolder.getSafeActiveSession() ?: return + + when (vectorPreferences.getFdroidSyncBackgroundMode()) { + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> { + // we rely on periodic worker + Timber.i("## Sync: Work scheduled to periodically sync in ${vectorPreferences.backgroundSyncDelay()}s") + activeSession.startAutomaticBackgroundSync( + vectorPreferences.backgroundSyncTimeOut().toLong(), + vectorPreferences.backgroundSyncDelay().toLong() + ) + } + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> { + // We need to use alarm in this mode + AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, vectorPreferences.backgroundSyncDelay()) + Timber.i("## Sync: Alarm scheduled to start syncing") + } + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { + // we do nothing + Timber.i("## Sync: background sync is disabled") + } + } + } + } +} diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt index 19a6870b2b..510ade0a33 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt @@ -15,29 +15,30 @@ */ package im.vector.app.fdroid.features.settings.troubleshoot -import androidx.fragment.app.Fragment +import androidx.appcompat.app.AppCompatActivity import im.vector.app.R +import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TroubleshootTest +import javax.inject.Inject -// Not used anymore -class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { +class TestBatteryOptimization @Inject constructor( + private val context: AppCompatActivity, + private val stringProvider: StringProvider +) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { override fun perform() { - val context = fragment.context - if (context != null && isIgnoringBatteryOptimizations(context)) { - description = fragment.getString(R.string.settings_troubleshoot_test_battery_success) + if (isIgnoringBatteryOptimizations(context)) { + description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_success) status = TestStatus.SUCCESS quickFix = null } else { - description = fragment.getString(R.string.settings_troubleshoot_test_battery_failed) + description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_failed) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) { override fun doFix() { - fragment.activity?.let { - requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX) - } + requestDisablingBatteryOptimization(context, null, NotificationTroubleshootTestManager.REQ_CODE_FIX) } } status = TestStatus.FAILED diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt index 1af92ff387..674e7dfef5 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/AlarmSyncBroadcastReceiver.kt @@ -22,16 +22,18 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import android.os.PowerManager import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.services.VectorSyncService -import androidx.core.content.getSystemService +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.internal.session.sync.job.SyncService import timber.log.Timber class AlarmSyncBroadcastReceiver : BroadcastReceiver() { + lateinit var vectorPreferences: VectorPreferences + override fun onReceive(context: Context, intent: Intent) { val appContext = context.applicationContext if (appContext is HasVectorInjector) { @@ -40,41 +42,35 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { Timber.v("No active session don't launch sync service.") return } - } - - // Acquire a lock to give enough time for the sync :/ - context.getSystemService()!!.run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply { - acquire((10_000).toLong()) - } + vectorPreferences = appContext.injector().vectorPreferences() } val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return // This method is called when the BroadcastReceiver is receiving an Intent broadcast. Timber.d("RestartBroadcastReceiver received intent") - VectorSyncService.newIntent(context, sessionId).let { + VectorSyncService.newPeriodicIntent(context, sessionId, vectorPreferences.backgroundSyncTimeOut(), vectorPreferences.backgroundSyncDelay()).let { try { ContextCompat.startForegroundService(context, it) } catch (ex: Throwable) { - // TODO + Timber.i("## Sync: Failed to start service, Alarm scheduled to restart service") + scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay()) Timber.e(ex) } } - - scheduleAlarm(context, sessionId, 30_000L) - Timber.i("Alarm scheduled to restart service") } companion object { private const val REQUEST_CODE = 0 - fun scheduleAlarm(context: Context, sessionId: String, delay: Long) { + fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int) { // Reschedule + Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply { putExtra(SyncService.EXTRA_SESSION_ID, sessionId) + putExtra(SyncService.EXTRA_PERIODIC, true) } val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) - val firstMillis = System.currentTimeMillis() + delay + val firstMillis = System.currentTimeMillis() + delayInSeconds * 1000L val alarmMgr = context.getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent) @@ -84,11 +80,20 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() { } fun cancelAlarm(context: Context) { - Timber.v("Cancel alarm") + Timber.v("## Sync: Cancel alarm for background sync") val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java) val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) val alarmMgr = context.getSystemService()!! alarmMgr.cancel(pIntent) + + // Stop current service to restart + VectorSyncService.stopIntent(context).let { + try { + ContextCompat.startForegroundService(context, it) + } catch (ex: Throwable) { + Timber.i("## Sync: Cancel sync") + } + } } } } diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt index a8a394576b..797b5734a2 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/receiver/OnApplicationUpgradeOrRebootReceiver.kt @@ -21,6 +21,8 @@ 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() { @@ -29,10 +31,11 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { Timber.v("## onReceive() ${intent.action}") val appContext = context.applicationContext if (appContext is HasVectorInjector) { - val activeSession = appContext.injector().activeSessionHolder().getSafeActiveSession() - if (activeSession != null) { - AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, 10) - } + BackgroundSyncStarter.start( + context, + appContext.vectorComponent().vectorPreferences(), + appContext.injector().activeSessionHolder() + ) } } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7a8c5fa134..5f0ee396ee 100755 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt @@ -22,9 +22,9 @@ 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 -import timber.log.Timber /** * This class has an alter ego in the gplay variant. @@ -61,16 +61,13 @@ object FcmHelper { // No op } - fun onEnterForeground(context: Context) { + 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) { - // We need to use alarm in this mode - if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { - val currentSession = activeSessionHolder.getActiveSession() - AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.sessionId, 4_000L) - Timber.i("Alarm scheduled to restart service") - } + BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder) } } diff --git a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt index ce9bad6c26..65b8609446 100644 --- a/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt +++ b/vector/src/fdroid/java/im/vector/app/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.push.fcm import androidx.fragment.app.Fragment import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot import im.vector.app.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions +import im.vector.app.fdroid.features.settings.troubleshoot.TestBatteryOptimization import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.TestAccountSettings import im.vector.app.features.settings.troubleshoot.TestDeviceSettings @@ -30,7 +31,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val private val testDeviceSettings: TestDeviceSettings, private val testPushRulesSettings: TestPushRulesSettings, private val testAutoStartBoot: TestAutoStartBoot, - private val testBackgroundRestrictions: TestBackgroundRestrictions) { + private val testBackgroundRestrictions: TestBackgroundRestrictions, + private val testBatteryOptimization: TestBatteryOptimization) { fun create(fragment: Fragment): NotificationTroubleshootTestManager { val mgr = NotificationTroubleshootTestManager(fragment) @@ -40,6 +42,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val mgr.addTest(testPushRulesSettings) mgr.addTest(testAutoStartBoot) mgr.addTest(testBackgroundRestrictions) + mgr.addTest(testBatteryOptimization) return mgr } } diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 84ca392c45..d139cad9d3 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -102,7 +102,7 @@ object FcmHelper { } @Suppress("UNUSED_PARAMETER") - fun onEnterForeground(context: Context) { + fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) { // No op } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index c9d2c96223..4b0ef66459 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -146,7 +146,7 @@ class VectorApplication : @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { Timber.i("App entered foreground") - FcmHelper.onEnterForeground(appContext) + FcmHelper.onEnterForeground(appContext, activeSessionHolder) activeSessionHolder.getSafeActiveSession()?.also { it.stopAnyBackgroundSync() } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index 6f0fd51525..cb87947612 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -37,7 +37,8 @@ fun Session.configureAndStart(context: Context) { fun Session.startSyncing(context: Context) { val applicationContext = context.applicationContext if (!hasAlreadySynced()) { - VectorSyncService.newIntent(applicationContext, sessionId).also { + // initial sync is done as a service so it can continue below app lifecycle + VectorSyncService.newOneShotIntent(applicationContext, sessionId, 0).also { try { ContextCompat.startForegroundService(applicationContext, it) } catch (ex: Throwable) { diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt index 20ceb07fb5..bf78d5b7fb 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt @@ -21,19 +21,56 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.getSystemService +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.Worker +import androidx.work.WorkerParameters import im.vector.app.R import im.vector.app.core.extensions.vectorComponent import im.vector.app.features.notifications.NotificationUtils import org.matrix.android.sdk.internal.session.sync.job.SyncService +import timber.log.Timber class VectorSyncService : SyncService() { companion object { - fun newIntent(context: Context, sessionId: String): Intent { + fun newOneShotIntent(context: Context, sessionId: String, timeoutSeconds: Int): Intent { return Intent(context, VectorSyncService::class.java).also { it.putExtra(EXTRA_SESSION_ID, sessionId) + it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds) + it.putExtra(EXTRA_PERIODIC, false) + } + } + + fun newPeriodicIntent(context: Context, sessionId: String, timeoutSeconds: Int, delayInSeconds: Int): Intent { + return Intent(context, VectorSyncService::class.java).also { + it.putExtra(EXTRA_SESSION_ID, sessionId) + it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds) + it.putExtra(EXTRA_PERIODIC, true) + it.putExtra(EXTRA_DELAY_SECONDS, delayInSeconds) + } + } + + fun newPeriodicNetworkBackIntent(context: Context, sessionId: String, timeoutSeconds: Int, delayInSeconds: Int): Intent { + return Intent(context, VectorSyncService::class.java).also { + it.putExtra(EXTRA_SESSION_ID, sessionId) + it.putExtra(EXTRA_TIMEOUT_SECONDS, timeoutSeconds) + it.putExtra(EXTRA_PERIODIC, true) + it.putExtra(EXTRA_DELAY_SECONDS, delayInSeconds) + it.putExtra(EXTRA_NETWORK_BACK_RESTART, true) + } + } + + fun stopIntent(context: Context): Intent { + return Intent(context, VectorSyncService::class.java).also { + it.action = ACTION_STOP } } } @@ -55,8 +92,30 @@ class VectorSyncService : SyncService() { startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } - override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) { - reschedule(sessionId, delay) + override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) { + reschedule(sessionId, timeout, delay) + } + + override fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) { + Timber.d("## Sync: A network error occured during sync") + val uploadWorkRequest: WorkRequest = + OneTimeWorkRequestBuilder() + .setInputData(Data.Builder() + .putString("sessionId", sessionId) + .putInt("timeout", timeout) + .putInt("delay", delay) + .build() + ) + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + + Timber.d("## Sync: Schedule a work to restart service when network will be on") + WorkManager + .getInstance(applicationContext) + .enqueue(uploadWorkRequest) } override fun onDestroy() { @@ -69,13 +128,13 @@ class VectorSyncService : SyncService() { notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) } - private fun reschedule(sessionId: String, delay: Long) { + private fun reschedule(sessionId: String, timeout: Int, delay: Int) { val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0) + PendingIntent.getForegroundService(this, 0, newPeriodicIntent(this, sessionId, timeout, delay), 0) } else { - PendingIntent.getService(this, 0, newIntent(this, sessionId), 0) + PendingIntent.getService(this, 0, newPeriodicIntent(this, sessionId, timeout, delay), 0) } - val firstMillis = System.currentTimeMillis() + delay + val firstMillis = System.currentTimeMillis() + delay * 1000L val alarmMgr = getSystemService()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) @@ -83,4 +142,28 @@ class VectorSyncService : SyncService() { alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) } } + + class RestartWhenNetworkOn(appContext: Context, workerParams: WorkerParameters) : + Worker(appContext, workerParams) { + override fun doWork(): Result { + val sessionId = inputData.getString("sessionId") ?: return Result.failure() + val timeout = inputData.getInt("timeout", 6) + val delay = inputData.getInt("delay", 60) + + val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + PendingIntent.getForegroundService(applicationContext, 0, newPeriodicNetworkBackIntent(applicationContext, sessionId, timeout, delay), 0) + } else { + PendingIntent.getService(applicationContext, 0, newPeriodicNetworkBackIntent(applicationContext, sessionId, timeout, delay), 0) + } + val firstMillis = System.currentTimeMillis() + delay * 1000L + val alarmMgr = getSystemService(applicationContext, AlarmManager::class.java)!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) + } else { + alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) + } + // Indicate whether the work finished successfully with the Result + return Result.success() + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncMode.kt b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncMode.kt new file mode 100644 index 0000000000..9d8b5755b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncMode.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings + +/** + * Different strategies for Background sync, only applicable to F-Droid version of the app + */ +enum class BackgroundSyncMode { + /** + * In this mode background syncs are scheduled via Workers, meaning that the system will have control on the periodicity + * of syncs when battery is low or when the phone is idle (sync will occur in allowed maintenance windows). After completion + * the sync work will schedule another one. + */ + FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY, + + /** + * This mode requires the app to be exempted from battery optimization. Alarms will be launched and will wake up the app + * in order to perform the background sync as a foreground service. After completion the service will schedule another alarm + */ + FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME, + + /** + * The app won't sync in background + */ + FDROID_BACKGROUND_SYNC_MODE_DISABLED; + + companion object { + const val DEFAULT_SYNC_DELAY_SECONDS = 60 + const val DEFAULT_SYNC_TIMEOUT_SECONDS = 6 + + fun fromString(value: String?): BackgroundSyncMode = values().firstOrNull { it.name == value } + ?: FDROID_BACKGROUND_SYNC_MODE_DISABLED + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncModeChooserDialog.kt b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncModeChooserDialog.kt new file mode 100644 index 0000000000..59b8569c1e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncModeChooserDialog.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import im.vector.app.R + +class BackgroundSyncModeChooserDialog : DialogFragment() { + + var interactionListener: InteractionListener? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val initialMode = BackgroundSyncMode.fromString(arguments?.getString(ARG_INITIAL_MODE)) + + val view = requireActivity().layoutInflater.inflate(R.layout.dialog_background_sync_mode, null) + val dialog = AlertDialog.Builder(requireActivity()) + .setTitle(R.string.settings_background_fdroid_sync_mode) + .setView(view) + .setPositiveButton(R.string.cancel, null) + .create() + + view.findViewById(R.id.backgroundSyncModeBattery).setOnClickListener { + interactionListener + ?.takeIf { initialMode != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY } + ?.onOptionSelected(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY) + dialog.dismiss() + } + view.findViewById(R.id.backgroundSyncModeReal).setOnClickListener { + interactionListener + ?.takeIf { initialMode != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME } + ?.onOptionSelected(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) + dialog.dismiss() + } + view.findViewById(R.id.backgroundSyncModeOff).setOnClickListener { + interactionListener + ?.takeIf { initialMode != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED } + ?.onOptionSelected(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED) + dialog.dismiss() + } + return dialog + } + + interface InteractionListener { + fun onOptionSelected(mode: BackgroundSyncMode) + } + + companion object { + private const val ARG_INITIAL_MODE = "ARG_INITIAL_MODE" + + fun newInstance(selectedMode: BackgroundSyncMode): BackgroundSyncModeChooserDialog { + val frag = BackgroundSyncModeChooserDialog() + val args = Bundle() + args.putString(ARG_INITIAL_MODE, selectedMode.name) + frag.arguments = args + return frag + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 886395c1f7..7415b57310 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -55,6 +55,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS" const val SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" const val SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_FDROID_BACKGROUND_SYNC_MODE = "SETTINGS_FDROID_BACKGROUND_SYNC_MODE" const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" @@ -182,6 +183,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" + // Background sync modes + // some preferences keys must be kept after a logout private val mKeysToKeepAfterLogout = listOf( SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, @@ -830,4 +833,53 @@ class VectorPreferences @Inject constructor(private val context: Context) { fun useFlagPinCode(): Boolean { return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false) } + + fun backgroundSyncTimeOut(): Int { + return tryThis { + // The xml pref is saved as a string so use getString and parse + defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt() + } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS + } + + fun setBackgroundSyncTimeout(timeInSecond: Int) { + defaultPrefs + .edit() + .putString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeInSecond.toString()) + .apply() + } + + fun backgroundSyncDelay(): Int { + return tryThis { + // The xml pref is saved as a string so use getString and parse + defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt() + } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS + } + + fun setBackgroundSyncDelay(timeInSecond: Int) { + defaultPrefs + .edit() + .putString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, timeInSecond.toString()) + .apply() + } + + fun isBackgroundSyncEnabled(): Boolean { + return getFdroidSyncBackgroundMode() != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + } + + fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + defaultPrefs + .edit() + .putString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, mode.name) + .apply() + } + + fun getFdroidSyncBackgroundMode(): BackgroundSyncMode { + return try { + val strPref = defaultPrefs + .getString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY.name) + BackgroundSyncMode.values().firstOrNull { it.name == strPref } ?: BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + } catch (e: Throwable) { + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt index 1cf9b0dcfa..3f74c4db75 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt @@ -25,16 +25,21 @@ import android.os.Parcelable import android.widget.Toast import androidx.preference.Preference import androidx.preference.SwitchPreference -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.pushrules.RuleIds -import org.matrix.android.sdk.api.pushrules.RuleKind import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.preference.VectorEditTextPreference import im.vector.app.core.preference.VectorPreference +import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.utils.isIgnoringBatteryOptimizations +import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.features.notifications.NotificationUtils import im.vector.app.push.fcm.FcmHelper +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.pushrules.RuleIds +import org.matrix.android.sdk.api.pushrules.RuleKind import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml @@ -42,7 +47,8 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( private val pushManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences -) : VectorSettingsBaseFragment() { +) : VectorSettingsBaseFragment(), + BackgroundSyncModeChooserDialog.InteractionListener { override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications @@ -65,9 +71,99 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel } + findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val initialMode = vectorPreferences.getFdroidSyncBackgroundMode() + val dialogFragment = BackgroundSyncModeChooserDialog.newInstance(initialMode) + dialogFragment.interactionListener = this + activity?.supportFragmentManager?.let { fm -> + dialogFragment.show(fm, "syncDialog") + } + true + } + } + + findPreference(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let { + it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() + it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut()) + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (newValue is String) { + val syncTimeout = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS + vectorPreferences.setBackgroundSyncTimeout(maxOf(0, syncTimeout)) + refreshBackgroundSyncPrefs() + } + true + } + } + + findPreference(VectorPreferences.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY)?.let { + it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() + it.summary = secondsToText(vectorPreferences.backgroundSyncDelay()) + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (newValue is String) { + val syncDelay = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS + vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay)) + refreshBackgroundSyncPrefs() + } + true + } + } + + refreshBackgroundSyncPrefs() + handleSystemPreference() } + // BackgroundSyncModeChooserDialog.InteractionListener + override fun onOptionSelected(mode: BackgroundSyncMode) { + // option has change, need to act + if (mode == BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) { + // Important, Battery optim white listing is needed in this mode; + // Even if using foreground service with foreground notif, it stops to work + // in doze mode for certain devices :/ + if (!isIgnoringBatteryOptimizations(requireContext())) { + requestDisablingBatteryOptimization(requireActivity(), + this@VectorSettingsNotificationPreferenceFragment, + REQUEST_BATTERY_OPTIMIZATION) + } + } + vectorPreferences.setFdroidSyncBackgroundMode(mode) + refreshBackgroundSyncPrefs() + } + + private fun refreshBackgroundSyncPrefs() { + findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { + it.summary = when (vectorPreferences.getFdroidSyncBackgroundMode()) { + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> getString(R.string.settings_background_fdroid_sync_mode_battery) + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> getString(R.string.settings_background_fdroid_sync_mode_real_time) + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> getString(R.string.settings_background_fdroid_sync_mode_disabled) + } + } + + findPreference(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let { + it.isVisible = !FcmHelper.isPushSupported() + } + + findPreference(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let { + it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() + it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut()) + } + findPreference(VectorPreferences.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY)?.let { + it.isEnabled = vectorPreferences.isBackgroundSyncEnabled() + it.summary = secondsToText(vectorPreferences.backgroundSyncDelay()) + } + } + + /** + * Convert a delay in seconds to string + * + * @param seconds the delay in seconds + * @return the text + */ + private fun secondsToText(seconds: Int): String { + return resources.getQuantityString(R.plurals.seconds, seconds, seconds) + } + private fun handleSystemPreference() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!! if (NotificationUtils.supportNotificationChannels()) { @@ -148,6 +244,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( val preference = findPreference(key) preference?.isHighlighted = true } + + refreshPref() + } + + private fun refreshPref() { + // This pref may have change from troubleshoot pref fragment + if (!FcmHelper.isPushSupported()) { + findPreference(VectorPreferences.SETTINGS_START_ON_BOOT_PREFERENCE_KEY) + ?.isChecked = vectorPreferences.autoStartOnBoot() + } } override fun onAttach(context: Context) { @@ -155,6 +261,9 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( if (context is VectorSettingsFragmentInteractionListener) { interactionListener = context } + (activity?.supportFragmentManager + ?.findFragmentByTag("syncDialog") as BackgroundSyncModeChooserDialog?) + ?.interactionListener = this } override fun onDetach() { @@ -234,5 +343,6 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( companion object { private const val REQUEST_NOTIFICATION_RINGTONE = 888 + private const val REQUEST_BATTERY_OPTIMIZATION = 500 } } diff --git a/vector/src/main/res/layout/dialog_background_sync_mode.xml b/vector/src/main/res/layout/dialog_background_sync_mode.xml new file mode 100644 index 0000000000..1ddaa3a3f6 --- /dev/null +++ b/vector/src/main/res/layout/dialog_background_sync_mode.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a0abc28a4d..1ed36e4228 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -797,7 +797,7 @@ Messages sent by bot Background synchronization - Background Sync Mode (Experimental) + Background Sync Mode Optimized for battery Element will sync in background in way that preserves the device’s limited resources (battery).\nDepending on your device resource state, the sync may be deferred by the operating system. Optimized for real time @@ -815,6 +815,10 @@ Delay between each Sync second seconds + + %d second + %d seconds + Version olm version diff --git a/vector/src/main/res/xml/network_security_config.xml b/vector/src/main/res/xml/network_security_config.xml index e40c61c229..1f323dffd1 100644 --- a/vector/src/main/res/xml/network_security_config.xml +++ b/vector/src/main/res/xml/network_security_config.xml @@ -13,4 +13,11 @@ 10.0.2.2 + + + + + + + diff --git a/vector/src/main/res/xml/vector_settings_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 3e19c9e9e5..983ddf36f2 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -62,6 +62,36 @@ + + + + + + + + + + + + +