android: workaround of system restricted background (#2873)

* android: workaround of system restricted background

* strings

* exclude lockscreen call from requirements to unrestrict

* texts

* added button and changed behaviour

* texts 2

* button instead of alert

* padding

* bigger padding

* refactor

* don't jump on button hide

* no exceptions for lockscreen

* sometimes do not show off alert

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-08-09 21:56:53 +03:00
committed by GitHub
parent 38dc14f041
commit 9543af4784
9 changed files with 282 additions and 35 deletions
@@ -93,12 +93,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
(!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isBackgroundAllowed())
}
private fun allowToStartPeriodically() = with(chatModel.controller) {
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC &&
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations())
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isBackgroundAllowed())
}
/*
@@ -158,7 +158,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
if (mode.requiresIgnoringBattery && !SimplexService.isIgnoringBatteryOptimizations()) {
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
appPrefs.backgroundServiceNoticeShown.set(false)
}
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
@@ -172,7 +172,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
SimplexService.showBackgroundServiceNoticeIfNeeded()
SimplexService.showBackgroundServiceNoticeIfNeeded(showOffAlert = false)
}
override fun androidChatStartedAfterBeingOff() {
@@ -199,6 +199,19 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
}
}
override fun androidIsBackgroundCallAllowed(): Boolean = !SimplexService.isBackgroundRestricted()
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
if (SimplexService.isBackgroundRestricted()) {
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
SimplexService.showBGRestrictedInCall {
userChoice.complete(it)
}
return userChoice.await()
}
return true
}
}
}
}
@@ -9,6 +9,7 @@ import android.os.*
import android.provider.Settings
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -17,7 +18,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.common.AppLock
import chat.simplex.common.AppLock.clearAuthState
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.NotificationsMode
@@ -50,6 +50,7 @@ class SimplexService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
return START_STICKY // to restart if killed
}
@@ -353,7 +354,7 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
fun showBackgroundServiceNoticeIfNeeded() {
fun showBackgroundServiceNoticeIfNeeded(showOffAlert: Boolean = true) {
val appPrefs = ChatController.appPrefs
val mode = appPrefs.notificationsMode.get()
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
@@ -362,27 +363,28 @@ class SimplexService: Service() {
if (!appPrefs.backgroundServiceNoticeShown.get()) {
// the branch for the new users who have never seen service notice
if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations()) {
if (!mode.requiresIgnoringBattery || isBackgroundAllowed()) {
showBGServiceNotice(mode)
} else {
showBGServiceNoticeIgnoreOptimization(mode)
} else if (isBackgroundRestricted()) {
showBGServiceNoticeSystemRestricted(mode, showOffAlert)
} else if (!isIgnoringBatteryOptimizations()) {
showBGServiceNoticeIgnoreOptimization(mode, showOffAlert)
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
appPrefs.backgroundServiceNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
} else if (mode.requiresIgnoringBattery && isBackgroundRestricted()) {
// the branch for users who have app installed, and have seen the service notice,
// but the service is running AND system background restriction is on OR the battery optimization for the app is in RESTRICTED state
showBGServiceNoticeSystemRestricted(mode, showOffAlert)
if (!appPrefs.backgroundServiceBatteryNoticeShown.get()) {
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
}
} else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service
showDisablingServiceNotice(mode)
appPrefs.notificationsMode.set(NotificationsMode.OFF)
StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll()
safeStopService()
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization(mode)
// but the battery optimization for the app is in OPTIMIZED state (Android 12+) AND the service is running
showBGServiceNoticeIgnoreOptimization(mode, showOffAlert)
if (!appPrefs.backgroundServiceBatteryNoticeShown.get()) {
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
}
} else {
@@ -425,13 +427,17 @@ class SimplexService: Service() {
)
}
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode) = AlertManager.shared.showAlert {
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
val ignoreOptimization = {
AlertManager.shared.hideAlert()
askAboutIgnoringBatteryOptimization()
}
val disableNotifications = {
AlertManager.shared.hideAlert()
disableNotifications(mode, showOffAlert)
}
AlertDialog(
onDismissRequest = ignoreOptimization,
onDismissRequest = disableNotifications,
title = {
Row {
Icon(
@@ -454,12 +460,98 @@ class SimplexService: Service() {
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
}
},
dismissButton = {
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
},
confirmButton = {
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.ok)) }
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) }
}
)
}
private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
val unrestrict = {
AlertManager.shared.hideAlert()
askToUnrestrictBackground()
}
val disableNotifications = {
AlertManager.shared.hideAlert()
disableNotifications(mode, showOffAlert)
}
AlertDialog(
onDismissRequest = disableNotifications,
title = {
Row {
Icon(
painterResource(MR.images.ic_bolt),
contentDescription =
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
)
Text(
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
Text(
annotatedStringResource(MR.strings.system_restricted_background_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.system_restricted_background_warn))
}
},
dismissButton = {
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
},
confirmButton = {
TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) }
}
)
}
fun showBGRestrictedInCall(onDismiss: (allowedCall: Boolean) -> Unit) = AlertManager.shared.showAlert {
val unrestrict = {
askToUnrestrictBackground()
}
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Text(
stringResource(MR.strings.system_restricted_background_in_call_title),
fontWeight = FontWeight.Bold
)
},
text = {
Column {
Text(
annotatedStringResource(MR.strings.system_restricted_background_in_call_desc),
Modifier.padding(bottom = 8.dp)
)
Text(annotatedStringResource(MR.strings.system_restricted_background_in_call_warn))
}
},
confirmButton = {
TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) }
}
)
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
scope.launch {
repeat(10000) {
delay(200)
if (!isBackgroundRestricted()) {
AlertManager.shared.hideAlert()
}
}
}
onDispose {
onDismiss(!isBackgroundRestricted())
}
}
}
private fun showDisablingServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
@@ -490,11 +582,22 @@ class SimplexService: Service() {
)
}
fun isBackgroundAllowed(): Boolean = isIgnoringBatteryOptimizations() && !isBackgroundRestricted()
fun isIgnoringBatteryOptimizations(): Boolean {
val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName)
}
fun isBackgroundRestricted(): Boolean {
return if (Build.VERSION.SDK_INT >= 28) {
val activityService = androidAppContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager
activityService.isBackgroundRestricted
} else {
false
}
}
private fun askAboutIgnoringBatteryOptimization() {
Intent().apply {
@SuppressLint("BatteryLife")
@@ -505,5 +608,29 @@ class SimplexService: Service() {
androidAppContext.startActivity(this)
}
}
private fun askToUnrestrictBackground() {
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${androidAppContext.packageName}")
// This flag is needed when you start a new activity from non-Activity context
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
androidAppContext.startActivity(this)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.stackTraceToString())
}
}
}
private fun disableNotifications(mode: NotificationsMode, showOffAlert: Boolean) {
if (showOffAlert) {
showDisablingServiceNotice(mode)
}
ChatController.appPrefs.notificationsMode.set(NotificationsMode.OFF)
StartReceiver.toggleReceiver(false)
MessagesFetcherWorker.cancelAll()
safeStopService()
}
}
}