diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 5062b90ed6..9d3d94390d 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ + SimplexService.start(applicationContext) + Lifecycle.Event.ON_RESUME -> + if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { + chatController.showBackgroundServiceNoticeIfNeeded() + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 9f2e2835e8..daa0e62041 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1,9 +1,14 @@ package chat.simplex.app.model +import android.annotation.SuppressLint import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo -import android.content.Context -import android.content.SharedPreferences +import android.app.Application +import android.content.* +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -13,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat.startActivityForResult import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.views.call.* @@ -526,38 +532,118 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt chatModel.updateNetworkStatus(contact.id, Chat.NetworkStatus.Error(err)) } - fun showBackgroundServiceNotice() { + fun showBackgroundServiceNoticeIfNeeded() { + Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") if (!getBackgroundServiceNoticeShown()) { - AlertManager.shared.showAlert { - AlertDialog( - onDismissRequest = AlertManager.shared::hideAlert, - title = { - Row { - Icon( - Icons.Outlined.Bolt, - contentDescription = stringResource(R.string.icon_descr_instant_notifications), - ) - Text(stringResource(R.string.private_instant_notifications), fontWeight = FontWeight.Bold) - } - }, - text = { - Column { - Text( - annotatedStringResource(R.string.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery), - Modifier.padding(bottom = 8.dp) - ) - Text(annotatedStringResource(R.string.it_can_disabled_via_settings_notifications_still_shown)) - } - }, - confirmButton = { - Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } - } - ) + // the branch for the new users who has never seen service notice + if (isIgnoringBatteryOptimizations(appContext)) { + showBGServiceNotice() + } else { + showBGServiceNoticeIgnoreOptimization() + } + // set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice + setBackgroundServiceNoticeShown(true) + setBackgroundServiceBatteryNoticeShown(true) + } else if (!isIgnoringBatteryOptimizations(appContext) && getRunServiceInBackground()) { + // 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 (getBackgroundServiceBatteryNoticeShown()) { + // users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service + showDisablingServiceNotice() + setRunServiceInBackground(false) + chatModel.runServiceInBackground.value = false + } else { + // show battery optimization notice + showBGServiceNoticeIgnoreOptimization() + setBackgroundServiceBatteryNoticeShown(true) } - setBackgroundServiceNoticeShown() } } + private fun showBGServiceNotice() = AlertManager.shared.showAlert { + AlertDialog( + onDismissRequest = AlertManager.shared::hideAlert, + title = { + Row { + Icon( + Icons.Outlined.Bolt, + contentDescription = stringResource(R.string.icon_descr_instant_notifications), + ) + Text(stringResource(R.string.private_instant_notifications), fontWeight = FontWeight.Bold) + } + }, + text = { + Column { + Text( + annotatedStringResource(R.string.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery), + Modifier.padding(bottom = 8.dp) + ) + Text(annotatedStringResource(R.string.it_can_disabled_via_settings_notifications_still_shown)) + } + }, + confirmButton = { + Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + } + ) + } + + private fun showBGServiceNoticeIgnoreOptimization() = AlertManager.shared.showAlert { + val ignoreOptimization = { + AlertManager.shared.hideAlert() + askAboutIgnoringBatteryOptimization(appContext) + } + AlertDialog( + onDismissRequest = ignoreOptimization, + title = { + Row { + Icon( + Icons.Outlined.Bolt, + contentDescription = stringResource(R.string.icon_descr_instant_notifications), + ) + Text(stringResource(R.string.private_instant_notifications), fontWeight = FontWeight.Bold) + } + }, + text = { + Column { + Text( + annotatedStringResource(R.string.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery), + Modifier.padding(bottom = 8.dp) + ) + Text(annotatedStringResource(R.string.turn_off_battery_optimization)) + } + }, + confirmButton = { + Button(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) } + } + ) + } + + private fun showDisablingServiceNotice() = AlertManager.shared.showAlert { + AlertDialog( + onDismissRequest = AlertManager.shared::hideAlert, + title = { + Row { + Icon( + Icons.Outlined.Bolt, + contentDescription = stringResource(R.string.icon_descr_instant_notifications), + ) + Text(stringResource(R.string.private_instant_notifications_disabled), fontWeight = FontWeight.Bold) + } + }, + text = { + Column { + Text( + annotatedStringResource(R.string.turning_off_background_service), + Modifier.padding(bottom = 8.dp) + ) + } + }, + confirmButton = { + Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } + } + ) + } + fun getAutoRestartWorkerVersion(): Int = sharedPreferences.getInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0) fun setAutoRestartWorkerVersion(version: Int) = @@ -572,18 +658,45 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt .putBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, runService) .apply() - fun getBackgroundServiceNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) + private fun getBackgroundServiceNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) - fun setBackgroundServiceNoticeShown() = + fun setBackgroundServiceNoticeShown(shown: Boolean) = sharedPreferences.edit() - .putBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, true) + .putBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, shown) .apply() + private fun getBackgroundServiceBatteryNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) + + fun setBackgroundServiceBatteryNoticeShown(shown: Boolean) = + sharedPreferences.edit() + .putBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, shown) + .apply() + + fun isIgnoringBatteryOptimizations(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true + val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + private fun askAboutIgnoringBatteryOptimization(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return + + Intent().apply { + @SuppressLint("BatteryLife") + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${context.packageName}") + // This flag is needed when you start a new activity from non-Activity context + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(this) + } + } + companion object { private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground" private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown" + private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown" } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index d26f142f35..6703df1aa0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -115,8 +115,7 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) { ) chatModel.controller.startChat(user) SimplexService.start(chatModel.controller.appContext) - // TODO show it later? - chatModel.controller.showBackgroundServiceNotice() + chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 64f7610bc9..b09b6e7484 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -37,6 +37,10 @@ fun SettingsView(chatModel: ChatModel) { runServiceInBackground = chatModel.runServiceInBackground, setRunServiceInBackground = { on -> chatModel.controller.setRunServiceInBackground(on) + if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) { + chatModel.controller.setBackgroundServiceNoticeShown(false) + } + chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.runServiceInBackground.value = on }, showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 9496535853..9667b24111 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -54,8 +54,11 @@ Приватные мгновенные уведомления! + Приватные уведомления выключены! Чтобы защитить ваши личные данные, вместо уведомлений от сервера приложение запускает фоновый сервис SimpleX, который потребляет несколько процентов батареи в день. Он может быть выключен через Настройки – вы продолжите получать уведомления о сообщениях пока приложение запущено. + Для использования приватных уведомлений, пожалуйста отключите оптимизацию батареи для SimpleX в слеующем диалоге. Иначе уведомления будут выключены. + Оптимизация батареи включена, поэтому приватные уведомления выключены. Вы можете снова включить их через Настройки. SimpleX Chat сервис diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 5dce4ad6ea..7143c0e314 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -54,8 +54,11 @@ Private instant notifications! + Private notifications disabled! To preserve your privacy, instead of push notifications the app has a SimpleX background service – it uses a few percent of the battery per day. It can be disabled via settings – notifications will still be shown while the app is running. + In order to use it, please disable battery optimization for SimpleX in the next dialog. Otherwise, the notifications will be disabled. + Battery optimization is active, turning off Private notifications. You can re-enable them via settings. SimpleX Chat service