F-Droid background sync modes

This commit is contained in:
Valere 2020-09-08 12:28:29 +02:00 committed by Benoit Marty
parent b9e8d7187c
commit 971b425e17
21 changed files with 637 additions and 100 deletions

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 = 30L)
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,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<PowerManager>()?.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"
}
}

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

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

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

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

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

@ -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<AlarmManager>()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)

View File

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

View File

@ -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<ListView>(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<SyncMode>(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<TextView>(R.id.item_generic_title_text)?.let {
it.text = context.getString(syncMode.title)
}
itemView?.findViewById<TextView>(R.id.item_generic_description_text)?.let {
it.text = context.getString(syncMode.description)
it.isVisible = true
}
itemView?.findViewById<RadioButton>(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
}
}
}

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, "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
}
}
}

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
@ -65,9 +70,104 @@ 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,
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<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) } ?: 6
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) } ?: 6
vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay))
refreshBackgroundSyncPrefs()
}
true
}
}
refreshBackgroundSyncPrefs()
handleSystemPreference()
}
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 if (seconds > 1) {
seconds.toString() + " " + getString(R.string.settings_seconds)
} else {
seconds.toString() + " " + getString(R.string.settings_second)
}
}
private fun handleSystemPreference() {
val callNotificationsSystemOptions = findPreference<VectorPreference>(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
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/dialog_background_sync_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_custom_dialog_radio_line" />
</LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:minHeight="50dp">
<TextView
android:id="@+id/item_generic_title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="2dp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toStartOf="@+id/item_generic_radio"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Item Title"
tools:textSize="14sp" />
<TextView
android:id="@+id/item_generic_description_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingTop="2dp"
android:paddingBottom="4dp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_generic_radio"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/item_generic_title_text"
tools:text="At totam delectus et aliquid dolorem. Consectetur voluptas tempore et non blanditiis id optio. Dolorum impedit quidem minus nihil. "
tools:visibility="visible" />
<RadioButton
android:id="@+id/item_generic_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/item_generic_title_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

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>

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

@ -71,34 +71,37 @@
</im.vector.app.core.preference.VectorPreferenceCategory>
<!--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">
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.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
<!--im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /-->