Merge pull request #2080 from vector-im/feature/polling_work

Feature/polling work
This commit is contained in:
Benoit Marty 2020-09-11 15:39:49 +02:00 committed by GitHub
commit 61b91f4015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 744 additions and 132 deletions

View File

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

View File

@ -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()

View File

@ -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() {

View File

@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface SyncTask : Task<SyncTask.Params, Unit> {
data class Params(var timeout: Long = 30_000L)
data class Params(var timeout: Long = 6_000L)
}
internal class DefaultSyncTask @Inject constructor(

View File

@ -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<PowerManager>()?.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"
}
}

View File

@ -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<SyncWorker>()
.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<SyncWorker>()
.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) {

View File

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

View File

@ -4,6 +4,11 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!--
Required for long polling account synchronisation in background.
If not present ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent action won't work
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application>

View File

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

View File

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

View File

@ -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<PowerManager>()!!.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<AlarmManager>()!!
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<AlarmManager>()!!
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")
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -102,7 +102,7 @@ object FcmHelper {
}
@Suppress("UNUSED_PARAMETER")
fun onEnterForeground(context: Context) {
fun onEnterForeground(context: Context, activeSessionHolder: ActiveSessionHolder) {
// No op
}

View File

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

View File

@ -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) {

View File

@ -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<RestartWhenNetworkOn>()
.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<AlarmManager>()!!
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<AlarmManager>(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()
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<VectorPreference>(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<VectorEditTextPreference>(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<VectorEditTextPreference>(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<VectorPreference>(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<VectorPreferenceCategory>(VectorPreferences.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY)?.let {
it.isVisible = !FcmHelper.isPushSupported()
}
findPreference<VectorEditTextPreference>(VectorPreferences.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY)?.let {
it.isEnabled = vectorPreferences.isBackgroundSyncEnabled()
it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut())
}
findPreference<VectorEditTextPreference>(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<VectorPreference>(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!!
if (NotificationUtils.supportNotificationChannels()) {
@ -148,6 +244,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
val preference = findPreference<VectorSwitchPreference>(key)
preference?.isHighlighted = true
}
refreshPref()
}
private fun refreshPref() {
// This pref may have change from troubleshoot pref fragment
if (!FcmHelper.isPushSupported()) {
findPreference<VectorSwitchPreference>(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
}
}

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/backgroundSyncModeBattery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_background_fdroid_sync_mode_battery"
android:textColor="?riotx_text_primary"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/settings_background_fdroid_sync_mode_battery_description"
android:textColor="?riotx_text_secondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/backgroundSyncModeReal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_background_fdroid_sync_mode_real_time"
android:textColor="?riotx_text_primary"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/settings_background_fdroid_sync_mode_real_time_description"
android:textColor="?riotx_text_secondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/backgroundSyncModeOff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_background_fdroid_sync_mode_disabled"
android:textColor="?riotx_text_primary"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/settings_background_fdroid_sync_mode_disabled_description"
android:textColor="?riotx_text_secondary" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -797,7 +797,7 @@
<string name="settings_messages_sent_by_bot">Messages sent by bot</string>
<string name="settings_background_sync">Background synchronization</string>
<string name="settings_background_fdroid_sync_mode">Background Sync Mode (Experimental)</string>
<string name="settings_background_fdroid_sync_mode">Background Sync Mode</string>
<string name="settings_background_fdroid_sync_mode_battery">Optimized for battery</string>
<string name="settings_background_fdroid_sync_mode_battery_description">Element will sync in background in way that preserves the devices limited resources (battery).\nDepending on your device resource state, the sync may be deferred by the operating system.</string>
<string name="settings_background_fdroid_sync_mode_real_time">Optimized for real time</string>
@ -815,6 +815,10 @@
<string name="settings_set_sync_delay">Delay between each Sync</string>
<string name="settings_second">second</string>
<string name="settings_seconds">seconds</string>
<plurals name="seconds">
<item quantity="one">%d second</item>
<item quantity="other">%d seconds</item>
</plurals>
<string name="settings_version">Version</string>
<string name="settings_olm_version">olm version</string>

View File

@ -13,4 +13,11 @@
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>

View File

@ -62,6 +62,36 @@
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY"
android:title="@string/settings_background_sync"
app:isPreferenceVisible="false">
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_FDROID_BACKGROUND_SYNC_MODE"
android:persistent="false"
android:title="@string/settings_background_fdroid_sync_mode" />
<im.vector.app.core.preference.VectorEditTextPreference
android:inputType="numberDecimal"
android:persistent="false"
android:key="SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY"
android:title="@string/settings_set_sync_delay" />
<im.vector.app.core.preference.VectorEditTextPreference
android:inputType="numberDecimal"
android:persistent="false"
android:key="SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY"
android:title="@string/settings_set_sync_timeout" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
android:title="@string/settings_start_on_boot" />
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_troubleshoot_title">
<im.vector.app.core.preference.VectorPreference
@ -72,33 +102,6 @@
</im.vector.app.core.preference.VectorPreferenceCategory>
<!--im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY"
android:title="@string/settings_background_sync">
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_START_ON_BOOT_PREFERENCE_KEY"
android:title="@string/settings_start_on_boot" />
<im.vector.app.core.preference.VectorSwitchPreference
android:key="SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY"
android:title="@string/settings_enable_background_sync" />
<im.vector.app.core.preference.VectorEditTextPreference
android:dependency="SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY"
android:key="SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY"
android:numeric="integer"
android:title="@string/settings_set_sync_timeout" />
<im.vector.app.core.preference.VectorEditTextPreference
android:dependency="SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY"
android:key="SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY"
android:numeric="integer"
android:title="@string/settings_set_sync_delay" />
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /-->