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 🙌: Improvements 🙌:
- Handle date formatting properly (show time am/pm if needed, display year when needed) - Handle date formatting properly (show time am/pm if needed, display year when needed)
- Improve F-Droid Notification (#2055)
Bugfix 🐛: Bugfix 🐛:
- Clear the notification when the event is read elsewhere (#1822) - 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 :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * 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() fun stopAnyBackgroundSync()

View File

@ -166,8 +166,8 @@ internal class DefaultSession @Inject constructor(
SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) SyncWorker.requireBackgroundSync(workManagerProvider, sessionId)
} }
override fun startAutomaticBackgroundSync(repeatDelay: Long) { override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) {
SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds)
} }
override fun stopAnyBackgroundSync() { override fun stopAnyBackgroundSync() {

View File

@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface SyncTask : Task<SyncTask.Params, Unit> { 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( 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.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder 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.Matrix
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.SyncState 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.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers 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 timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -46,6 +49,11 @@ abstract class SyncService : Service() {
private var sessionId: String? = null private var sessionId: String? = null
private var mIsSelfDestroyed: Boolean = false 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 var isInitialSync: Boolean = false
private lateinit var session: Session private lateinit var session: Session
private lateinit var syncTask: SyncTask private lateinit var syncTask: SyncTask
@ -59,27 +67,60 @@ abstract class SyncService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob()) private val serviceScope = CoroutineScope(SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand $intent") Timber.i("## Sync: onStartCommand [$this] $intent with action: ${intent?.action}")
val isInit = initialize(intent)
if (isInit) { // We should start we have to ensure we fulfill contract to show notification
onStart(isInitialSync) // for foreground service (as per design for this service)
doSyncIfNotAlreadyRunning() // TODO can we check if it's really in foreground
} else { onStart(isInitialSync)
// We should start and stop as we have to ensure to call Service.startForeground() when (intent?.action) {
onStart(isInitialSync) ACTION_STOP -> {
stopMe() 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() { override fun onDestroy() {
Timber.i("## onDestroy() : $this") Timber.i("## Sync: onDestroy() [$this] periodic:$periodic preventReschedule:$preventReschedule")
if (!mIsSelfDestroyed) { if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this") Timber.d("## Sync: Destroy by the system : $this")
} }
serviceScope.coroutineContext.cancelChildren()
isRunning.set(false) 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() super.onDestroy()
} }
@ -90,9 +131,15 @@ abstract class SyncService : Service() {
private fun doSyncIfNotAlreadyRunning() { private fun doSyncIfNotAlreadyRunning() {
if (isRunning.get()) { 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 { } else {
isRunning.set(true) 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) { serviceScope.launch(coroutineDispatchers.io) {
doSync() doSync()
} }
@ -100,9 +147,10 @@ abstract class SyncService : Service() {
} }
private suspend fun doSync() { private suspend fun doSync() {
Timber.v("Execute sync request with timeout 0") Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds")
val params = SyncTask.Params(TIME_OUT) val params = SyncTask.Params(syncTimeoutSeconds * 1000L)
try { try {
// never do that in foreground, let the syncThread work
syncTask.execute(params) syncTask.execute(params)
// Start sync if we were doing an initial sync and the syncThread is not launched yet // Start sync if we were doing an initial sync and the syncThread is not launched yet
if (isInitialSync && session.getSyncState() == SyncState.Idle) { if (isInitialSync && session.getSyncState() == SyncState.Idle) {
@ -111,28 +159,34 @@ abstract class SyncService : Service() {
} }
stopMe() stopMe()
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable) Timber.e(throwable, "## Sync: sync service did fail ${isRunning.get()}")
if (throwable.isTokenError()) { if (throwable.isTokenError()) {
stopMe() // no need to retry
} else { preventReschedule = true
Timber.v("Should be rescheduled to avoid wasting resources")
sessionId?.also {
onRescheduleAsked(it, isInitialSync, delay = 10_000L)
}
stopMe()
} }
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 { private fun initialize(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
Timber.d("## Sync: initialize intent is null")
return false return false
} }
val matrix = Matrix.getInstance(applicationContext) val matrix = Matrix.getInstance(applicationContext)
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6)
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60)
try { try {
val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) 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() session = sessionComponent.session()
sessionId = safeSessionId sessionId = safeSessionId
syncTask = sessionComponent.syncTask() syncTask = sessionComponent.syncTask()
@ -143,14 +197,16 @@ abstract class SyncService : Service() {
backgroundDetectionObserver = matrix.backgroundDetectionObserver backgroundDetectionObserver = matrix.backgroundDetectionObserver
return true return true
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e(exception, "An exception occurred during initialisation") Timber.e(exception, "## Sync: An exception occurred during initialisation")
return false return false
} }
} }
abstract fun onStart(isInitialSync: Boolean) 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? { override fun onBind(intent: Intent?): IBinder? {
return null return null
@ -158,6 +214,11 @@ abstract class SyncService : Service() {
companion object { companion object {
const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" 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 java.util.concurrent.TimeUnit
import javax.inject.Inject 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 * Possible previous worker: None
@ -48,13 +49,15 @@ internal class SyncWorker(context: Context,
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, 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 override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var syncTask: SyncTask @Inject lateinit var syncTask: SyncTask
@Inject lateinit var taskExecutor: TaskExecutor @Inject lateinit var taskExecutor: TaskExecutor
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
@Inject lateinit var workManagerProvider: WorkManagerProvider
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.i("Sync work starting") Timber.i("Sync work starting")
@ -67,11 +70,21 @@ internal class SyncWorker(context: Context,
return runCatching { return runCatching {
doSync(params.timeout) doSync(params.timeout)
}.fold( }.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 -> { failure ->
if (failure.isTokenError() || !params.automaticallyRetry) { if (failure.isTokenError()) {
Result.failure() Result.failure()
} else { } 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() Result.retry()
} }
} }
@ -79,7 +92,7 @@ internal class SyncWorker(context: Context,
} }
private suspend fun doSync(timeout: Long) { private suspend fun doSync(timeout: Long) {
val taskParams = SyncTask.Params(timeout) val taskParams = SyncTask.Params(timeout * 1000)
syncTask.execute(taskParams) syncTask.execute(taskParams)
} }
@ -87,25 +100,27 @@ internal class SyncWorker(context: Context,
private const val BG_SYNC_WORK_NAME = "BG_SYNCP" private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { 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>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInputData(data) .setInputData(data)
.build() .build()
workManagerProvider.workManager 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) { fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) {
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setInputData(data) .setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) {

View File

@ -151,7 +151,7 @@ android\.app\.AlertDialog
new Gson\(\) new Gson\(\)
### Use matrixOneTimeWorkRequestBuilder ### Use matrixOneTimeWorkRequestBuilder
import androidx.work.OneTimeWorkRequestBuilder===1 import androidx.work.OneTimeWorkRequestBuilder===2
### Use TextUtils.formatFileSize ### Use TextUtils.formatFileSize
Formatter\.formatFileSize===1 Formatter\.formatFileSize===1
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # 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 ### 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 ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 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.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <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> <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 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.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.app.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TroubleshootTest import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import javax.inject.Inject
// Not used anymore class TestBatteryOptimization @Inject constructor(
class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { private val context: AppCompatActivity,
private val stringProvider: StringProvider
) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) {
override fun perform() { override fun perform() {
val context = fragment.context if (isIgnoringBatteryOptimizations(context)) {
if (context != null && isIgnoringBatteryOptimizations(context)) { description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_success)
description = fragment.getString(R.string.settings_troubleshoot_test_battery_success)
status = TestStatus.SUCCESS status = TestStatus.SUCCESS
quickFix = null quickFix = null
} else { } 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) { quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) {
override fun doFix() { override fun doFix() {
fragment.activity?.let { requestDisablingBatteryOptimization(context, null, NotificationTroubleshootTestManager.REQ_CODE_FIX)
requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
}
} }
} }
status = TestStatus.FAILED status = TestStatus.FAILED

View File

@ -22,16 +22,18 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.services.VectorSyncService 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 org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber import timber.log.Timber
class AlarmSyncBroadcastReceiver : BroadcastReceiver() { class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
lateinit var vectorPreferences: VectorPreferences
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val appContext = context.applicationContext val appContext = context.applicationContext
if (appContext is HasVectorInjector) { if (appContext is HasVectorInjector) {
@ -40,41 +42,35 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
Timber.v("No active session don't launch sync service.") Timber.v("No active session don't launch sync service.")
return return
} }
} vectorPreferences = appContext.injector().vectorPreferences()
// 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())
}
} }
val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return val sessionId = intent.getStringExtra(SyncService.EXTRA_SESSION_ID) ?: return
// This method is called when the BroadcastReceiver is receiving an Intent broadcast. // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Timber.d("RestartBroadcastReceiver received intent") Timber.d("RestartBroadcastReceiver received intent")
VectorSyncService.newIntent(context, sessionId).let { VectorSyncService.newPeriodicIntent(context, sessionId, vectorPreferences.backgroundSyncTimeOut(), vectorPreferences.backgroundSyncDelay()).let {
try { try {
ContextCompat.startForegroundService(context, it) ContextCompat.startForegroundService(context, it)
} catch (ex: Throwable) { } catch (ex: Throwable) {
// TODO Timber.i("## Sync: Failed to start service, Alarm scheduled to restart service")
scheduleAlarm(context, sessionId, vectorPreferences.backgroundSyncDelay())
Timber.e(ex) Timber.e(ex)
} }
} }
scheduleAlarm(context, sessionId, 30_000L)
Timber.i("Alarm scheduled to restart service")
} }
companion object { companion object {
private const val REQUEST_CODE = 0 private const val REQUEST_CODE = 0
fun scheduleAlarm(context: Context, sessionId: String, delay: Long) { fun scheduleAlarm(context: Context, sessionId: String, delayInSeconds: Int) {
// Reschedule // Reschedule
Timber.v("## Sync: Scheduling alarm for background sync in $delayInSeconds seconds")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply { val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java).apply {
putExtra(SyncService.EXTRA_SESSION_ID, sessionId) putExtra(SyncService.EXTRA_SESSION_ID, sessionId)
putExtra(SyncService.EXTRA_PERIODIC, true)
} }
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) 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>()!! val alarmMgr = context.getSystemService<AlarmManager>()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent) alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pIntent)
@ -84,11 +80,20 @@ class AlarmSyncBroadcastReceiver : BroadcastReceiver() {
} }
fun cancelAlarm(context: Context) { fun cancelAlarm(context: Context) {
Timber.v("Cancel alarm") Timber.v("## Sync: Cancel alarm for background sync")
val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java) val intent = Intent(context, AlarmSyncBroadcastReceiver::class.java)
val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT) val pIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr = context.getSystemService<AlarmManager>()!! val alarmMgr = context.getSystemService<AlarmManager>()!!
alarmMgr.cancel(pIntent) 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.Context
import android.content.Intent import android.content.Intent
import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.HasVectorInjector
import im.vector.app.core.extensions.vectorComponent
import im.vector.app.fdroid.BackgroundSyncStarter
import timber.log.Timber import timber.log.Timber
class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() { class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
@ -29,10 +31,11 @@ class OnApplicationUpgradeOrRebootReceiver : BroadcastReceiver() {
Timber.v("## onReceive() ${intent.action}") Timber.v("## onReceive() ${intent.action}")
val appContext = context.applicationContext val appContext = context.applicationContext
if (appContext is HasVectorInjector) { if (appContext is HasVectorInjector) {
val activeSession = appContext.injector().activeSessionHolder().getSafeActiveSession() BackgroundSyncStarter.start(
if (activeSession != null) { context,
AlarmSyncBroadcastReceiver.scheduleAlarm(context, activeSession.sessionId, 10) appContext.vectorComponent().vectorPreferences(),
} appContext.injector().activeSessionHolder()
)
} }
} }
} }

View File

@ -22,9 +22,9 @@ import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.fdroid.BackgroundSyncStarter
import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import timber.log.Timber
/** /**
* This class has an alter ego in the gplay variant. * This class has an alter ego in the gplay variant.
@ -61,16 +61,13 @@ object FcmHelper {
// No op // 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) AlarmSyncBroadcastReceiver.cancelAlarm(context)
} }
fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) {
// We need to use alarm in this mode BackgroundSyncStarter.start(context, vectorPreferences, activeSessionHolder)
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
val currentSession = activeSessionHolder.getActiveSession()
AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.sessionId, 4_000L)
Timber.i("Alarm scheduled to restart service")
}
} }
} }

View File

@ -18,6 +18,7 @@ package im.vector.app.push.fcm
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.app.fdroid.features.settings.troubleshoot.TestAutoStartBoot 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.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.NotificationTroubleshootTestManager
import im.vector.app.features.settings.troubleshoot.TestAccountSettings import im.vector.app.features.settings.troubleshoot.TestAccountSettings
import im.vector.app.features.settings.troubleshoot.TestDeviceSettings import im.vector.app.features.settings.troubleshoot.TestDeviceSettings
@ -30,7 +31,8 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
private val testDeviceSettings: TestDeviceSettings, private val testDeviceSettings: TestDeviceSettings,
private val testPushRulesSettings: TestPushRulesSettings, private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot, private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions) { private val testBackgroundRestrictions: TestBackgroundRestrictions,
private val testBatteryOptimization: TestBatteryOptimization) {
fun create(fragment: Fragment): NotificationTroubleshootTestManager { fun create(fragment: Fragment): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment) val mgr = NotificationTroubleshootTestManager(fragment)
@ -40,6 +42,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testPushRulesSettings) mgr.addTest(testPushRulesSettings)
mgr.addTest(testAutoStartBoot) mgr.addTest(testAutoStartBoot)
mgr.addTest(testBackgroundRestrictions) mgr.addTest(testBackgroundRestrictions)
mgr.addTest(testBatteryOptimization)
return mgr return mgr
} }
} }

View File

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

View File

@ -146,7 +146,7 @@ class VectorApplication :
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
Timber.i("App entered foreground") Timber.i("App entered foreground")
FcmHelper.onEnterForeground(appContext) FcmHelper.onEnterForeground(appContext, activeSessionHolder)
activeSessionHolder.getSafeActiveSession()?.also { activeSessionHolder.getSafeActiveSession()?.also {
it.stopAnyBackgroundSync() it.stopAnyBackgroundSync()
} }

View File

@ -37,7 +37,8 @@ fun Session.configureAndStart(context: Context) {
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
if (!hasAlreadySynced()) { 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 { try {
ContextCompat.startForegroundService(applicationContext, it) ContextCompat.startForegroundService(applicationContext, it)
} catch (ex: Throwable) { } catch (ex: Throwable) {

View File

@ -21,19 +21,56 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.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.R
import im.vector.app.core.extensions.vectorComponent import im.vector.app.core.extensions.vectorComponent
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import org.matrix.android.sdk.internal.session.sync.job.SyncService import org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber
class VectorSyncService : SyncService() { class VectorSyncService : SyncService() {
companion object { 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 { return Intent(context, VectorSyncService::class.java).also {
it.putExtra(EXTRA_SESSION_ID, sessionId) 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) startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
} }
override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) { override fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) {
reschedule(sessionId, delay) 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() { override fun onDestroy() {
@ -69,13 +128,13 @@ class VectorSyncService : SyncService() {
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE) 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) { 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 { } 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>()!! val alarmMgr = getSystemService<AlarmManager>()!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent)
@ -83,4 +142,28 @@ class VectorSyncService : SyncService() {
alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) 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_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS"
const val SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" 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_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_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY"
const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_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" 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" 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 // some preferences keys must be kept after a logout
private val mKeysToKeepAfterLogout = listOf( private val mKeysToKeepAfterLogout = listOf(
SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY,
@ -830,4 +833,53 @@ class VectorPreferences @Inject constructor(private val context: Context) {
fun useFlagPinCode(): Boolean { fun useFlagPinCode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false) 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 android.widget.Toast
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference 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.R
import im.vector.app.core.di.ActiveSessionHolder 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.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.isIgnoringBatteryOptimizations
import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.push.fcm.FcmHelper 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 import javax.inject.Inject
// Referenced in vector_settings_preferences_root.xml // Referenced in vector_settings_preferences_root.xml
@ -42,7 +47,8 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
private val pushManager: PushersManager, private val pushManager: PushersManager,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorSettingsBaseFragment() { ) : VectorSettingsBaseFragment(),
BackgroundSyncModeChooserDialog.InteractionListener {
override var titleRes: Int = R.string.settings_notifications override var titleRes: Int = R.string.settings_notifications
override val preferenceXmlRes = R.xml.vector_settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications
@ -65,9 +71,99 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
(pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel (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() 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() { private fun handleSystemPreference() {
val callNotificationsSystemOptions = findPreference<VectorPreference>(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!! val callNotificationsSystemOptions = findPreference<VectorPreference>(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!!
if (NotificationUtils.supportNotificationChannels()) { if (NotificationUtils.supportNotificationChannels()) {
@ -148,6 +244,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
val preference = findPreference<VectorSwitchPreference>(key) val preference = findPreference<VectorSwitchPreference>(key)
preference?.isHighlighted = true 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) { override fun onAttach(context: Context) {
@ -155,6 +261,9 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
if (context is VectorSettingsFragmentInteractionListener) { if (context is VectorSettingsFragmentInteractionListener) {
interactionListener = context interactionListener = context
} }
(activity?.supportFragmentManager
?.findFragmentByTag("syncDialog") as BackgroundSyncModeChooserDialog?)
?.interactionListener = this
} }
override fun onDetach() { override fun onDetach() {
@ -234,5 +343,6 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
companion object { companion object {
private const val REQUEST_NOTIFICATION_RINGTONE = 888 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_messages_sent_by_bot">Messages sent by bot</string>
<string name="settings_background_sync">Background synchronization</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">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_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> <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_set_sync_delay">Delay between each Sync</string>
<string name="settings_second">second</string> <string name="settings_second">second</string>
<string name="settings_seconds">seconds</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_version">Version</string>
<string name="settings_olm_version">olm 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 includeSubdomains="true">10.0.2.2</domain>
</domain-config> </domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config> </network-security-config>

View File

@ -62,6 +62,36 @@
</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"
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.VectorPreferenceCategory android:title="@string/settings_troubleshoot_title">
<im.vector.app.core.preference.VectorPreference <im.vector.app.core.preference.VectorPreference
@ -72,33 +102,6 @@
</im.vector.app.core.preference.VectorPreferenceCategory> </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">
<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:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /--> android:title="@string/settings_notifications_targets" /-->