From 971b425e177f31d65f2852325c4421c84516c471 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 8 Sep 2020 12:28:29 +0200 Subject: [PATCH] F-Droid background sync modes --- .../matrix/android/sdk/api/session/Session.kt | 2 +- .../sdk/internal/session/DefaultSession.kt | 4 +- .../sdk/internal/session/sync/SyncTask.kt | 2 +- .../internal/session/sync/job/SyncService.kt | 105 +++++++---- .../internal/session/sync/job/SyncWorker.kt | 37 ++-- vector/src/fdroid/AndroidManifest.xml | 5 + .../receiver/AlarmSyncBroadcastReceiver.kt | 39 ++-- .../java/im/vector/app/push/fcm/FcmHelper.kt | 28 ++- .../java/im/vector/app/push/fcm/FcmHelper.kt | 2 +- .../java/im/vector/app/VectorApplication.kt | 2 +- .../im/vector/app/core/extensions/Session.kt | 3 +- .../app/core/services/VectorSyncService.kt | 31 +++- .../features/settings/BackgroundSyncMode.kt | 28 +++ .../BackgroundSyncModeChooserDialog.kt | 172 ++++++++++++++++++ .../features/settings/VectorPreferences.kt | 52 ++++++ ...rSettingsNotificationPreferenceFragment.kt | 107 ++++++++++- .../layout/dialog_background_sync_mode.xml | 13 ++ .../layout/item_custom_dialog_radio_line.xml | 57 ++++++ vector/src/main/res/values/strings.xml | 2 +- .../main/res/xml/network_security_config.xml | 7 + .../res/xml/vector_settings_notifications.xml | 39 ++-- 21 files changed, 637 insertions(+), 100 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/BackgroundSyncMode.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/BackgroundSyncModeChooserDialog.kt create mode 100644 vector/src/main/res/layout/dialog_background_sync_mode.xml create mode 100644 vector/src/main/res/layout/item_custom_dialog_radio_line.xml 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..f5b10a5645 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 = 30L) 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..0ba1a672f9 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,6 +19,12 @@ 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.isTokenError import org.matrix.android.sdk.api.session.Session @@ -28,10 +34,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 +48,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 +66,51 @@ 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 + Timber.i("## Sync: command received, periodic: $periodic") + // default is syncing + doSyncIfNotAlreadyRunning() + } else { + Timber.i("## Sync: Failed to initialize service") + stopMe() + } + } } - // No intent just start the service, an alarm will should call with intent + return START_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) { + Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec") + onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds) + } super.onDestroy() } @@ -90,9 +121,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,8 +137,8 @@ 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 { syncTask.execute(params) // Start sync if we were doing an initial sync and the syncThread is not launched yet @@ -111,28 +148,28 @@ 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 } + // 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 +180,14 @@ 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) override fun onBind(intent: Intent?): IBinder? { return null @@ -158,6 +195,10 @@ 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 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/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/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/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/app/push/fcm/FcmHelper.kt index 7a8c5fa134..7874d0237f 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 @@ -23,6 +23,7 @@ import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager 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 @@ -61,16 +62,35 @@ 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") + when (vectorPreferences.getFdroidSyncBackgroundMode()) { + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY -> { + // we rely on periodic worker + Timber.i("## Sync: Work scheduled to periodically sync") + activeSessionHolder + .getSafeActiveSession() + ?.startAutomaticBackgroundSync( + vectorPreferences.backgroundSyncTimeOut().toLong(), + vectorPreferences.backgroundSyncDelay().toLong() + ) + } + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME -> { + val currentSession = activeSessionHolder.getActiveSession() + AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.sessionId, vectorPreferences.backgroundSyncDelay()) + Timber.i("## Sync: Alarm scheduled to start syncing") + } + BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED -> { + // we do nothing + } + } } } } 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..bbd8487ae5 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 @@ -31,9 +31,26 @@ 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 stopIntent(context: Context): Intent { + return Intent(context, VectorSyncService::class.java).also { + it.action = ACTION_STOP } } } @@ -55,8 +72,8 @@ 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 onDestroy() { @@ -69,13 +86,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) 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..f266060053 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncMode.kt @@ -0,0 +1,28 @@ +/* + * 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 + +enum class BackgroundSyncMode { + FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY, + FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME, + FDROID_BACKGROUND_SYNC_MODE_DISABLED; + + companion object { + 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..079be58fbc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/BackgroundSyncModeChooserDialog.kt @@ -0,0 +1,172 @@ +/* + * 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.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ListView +import android.widget.RadioButton +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity +import im.vector.app.R + +class BackgroundSyncModeChooserDialog : DialogFragment() { + + var interactionListener: InteractionListener? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return activity?.let { activity: FragmentActivity -> + val builder = AlertDialog.Builder(activity) + // Get the layout inflater + val inflater = activity.layoutInflater + + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + val view = inflater.inflate(R.layout.dialog_background_sync_mode, null) + view.findViewById(R.id.dialog_background_sync_list)?.let { + it.adapter = Adapter( + activity, + BackgroundSyncMode.fromString(arguments?.getString(ARG_INITIAL_MODE)) + ) + } + builder.setView(view) + // Add action buttons + .setPositiveButton(R.string.ok) { dialog, _ -> + val mode = getSelectedOption() + if (mode.name == arguments?.getString(ARG_INITIAL_MODE)) { + // it's like a cancel, no changes + dialog.cancel() + } else { + interactionListener?.onOptionSelected(mode) + dialog.dismiss() + } + } + .setNegativeButton(R.string.cancel) { dialog, _ -> + interactionListener?.onCancel() + dialog.cancel() + } + builder.create() + } ?: throw IllegalStateException("Activity cannot be null") + } + + private fun getSelectedOption(): BackgroundSyncMode { + options.forEach { + if (it.isSelected) return it.mode + } + // an item is always selected, should not happen + return options[0].mode + } + + data class SyncMode(val mode: BackgroundSyncMode, val title: Int, val description: Int, var isSelected: Boolean) + + private class Adapter(context: Context, val initialMode: BackgroundSyncMode) : ArrayAdapter(context, 0, options) { + init { + // mark the currently selected option + var initialModeFound = false + options.forEach { + it.isSelected = initialMode == it.mode + initialModeFound = true + } + if (!initialModeFound) { + options[0].isSelected = true + } + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val syncMode = getItem(position)!! + + // Only 3 items, let's keep it like that + val itemView = convertView + ?: LayoutInflater.from(context).inflate(R.layout.item_custom_dialog_radio_line, parent, false) + + // Lookup view for data population + itemView?.findViewById(R.id.item_generic_title_text)?.let { + it.text = context.getString(syncMode.title) + } + itemView?.findViewById(R.id.item_generic_description_text)?.let { + it.text = context.getString(syncMode.description) + it.isVisible = true + } + itemView?.findViewById(R.id.item_generic_radio)?.let { + it.isChecked = syncMode.isSelected + it.isVisible = true + // let the item click handle that + it.setOnClickListener { + toggleChangeAtPosition(position) + } + } + + itemView?.setOnClickListener { + toggleChangeAtPosition(position) + } + + // Populate the data into the template view using the data object + + return itemView + } + + private fun toggleChangeAtPosition(position: Int) { + if (getItem(position)?.isSelected == true) { + // nop + } else { + for (i in 0 until count) { + // we change the single selection + getItem(i)?.isSelected = i == position + } + notifyDataSetChanged() + } + } + } + + interface InteractionListener { + fun onCancel() {} + fun onOptionSelected(mode: BackgroundSyncMode) {} + } + + companion object { + private val options = listOf( + SyncMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY, + R.string.settings_background_fdroid_sync_mode_battery, + R.string.settings_background_fdroid_sync_mode_battery_description, false), + SyncMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME, + R.string.settings_background_fdroid_sync_mode_real_time, + R.string.settings_background_fdroid_sync_mode_real_time_description, false), + SyncMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED, + R.string.settings_background_fdroid_sync_mode_disabled, + R.string.settings_background_fdroid_sync_mode_disabled_description, false) + ) + + private const val ARG_INITIAL_MODE = "ARG_INITIAL_MODE" + + fun newInstance(selectedMode: BackgroundSyncMode, interactionListener: InteractionListener): BackgroundSyncModeChooserDialog { + val frag = BackgroundSyncModeChooserDialog() + frag.interactionListener = interactionListener + 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..2239efa992 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, "6")?.toInt() + } ?: 6 + } + + 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, "60")?.toInt() + } ?: 60 + } + + 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..21895e15b6 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 @@ -65,9 +70,104 @@ 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, + object : 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() + } + } + ) + activity?.supportFragmentManager?.let { + dialogFragment.show(it, "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) } ?: 6 + 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) } ?: 6 + vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay)) + refreshBackgroundSyncPrefs() + } + true + } + } + + refreshBackgroundSyncPrefs() + handleSystemPreference() } + 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 if (seconds > 1) { + seconds.toString() + " " + getString(R.string.settings_seconds) + } else { + seconds.toString() + " " + getString(R.string.settings_second) + } + } + private fun handleSystemPreference() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!! if (NotificationUtils.supportNotificationChannels()) { @@ -234,5 +334,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..89b2aaefde --- /dev/null +++ b/vector/src/main/res/layout/dialog_background_sync_mode.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/layout/item_custom_dialog_radio_line.xml b/vector/src/main/res/layout/item_custom_dialog_radio_line.xml new file mode 100644 index 0000000000..17d7be08ed --- /dev/null +++ b/vector/src/main/res/layout/item_custom_dialog_radio_line.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a0abc28a4d..c191b17f7e 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 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..52333dd81a 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -71,34 +71,37 @@ -