diff --git a/app/src/main/java/com/nextcloud/utils/BatteryOptimizationHelper.kt b/app/src/main/java/com/nextcloud/utils/BatteryOptimizationHelper.kt index eb4add0298c1..6bc1bb27cef8 100644 --- a/app/src/main/java/com/nextcloud/utils/BatteryOptimizationHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/BatteryOptimizationHelper.kt @@ -8,6 +8,8 @@ package com.nextcloud.utils import android.annotation.SuppressLint +import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.PowerManager @@ -15,32 +17,82 @@ import android.provider.Settings import androidx.core.net.toUri import com.owncloud.android.lib.common.utils.Log_OC +/** + * Helper for checking and requesting Android battery optimization exemptions. + * + * Methods allow checking whether the app is impacted by battery optimizations and + * guide the user to the appropriate settings screen to grant exemption. + */ object BatteryOptimizationHelper { private const val TAG = "BatteryOptimizationHelper" + /** + * Returns true if battery optimization is currently enabled for the app. + */ fun isBatteryOptimizationEnabled(context: Context): Boolean { val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager return !pm.isIgnoringBatteryOptimizations(context.packageName) } + /** + * Guides the user to disable battery optimization for this app. + * Returns true if a settings screen was opened, false if not possible. + */ @Suppress("TooGenericExceptionCaught") @SuppressLint("BatteryLife") - fun openBatteryOptimizationSettings(context: Context) { - try { + fun openBatteryOptimizationSettings(context: Context): Boolean { + return runCatching { + tryOpenIgnoreBatteryOptimizationsScreen(context) + || tryOpenGeneralBatteryOptimizationSettings(context) + }.getOrElse { e -> + Log_OC.w(TAG, "Failed to open battery optimization settings", e) + false + } + } + + private fun tryOpenIgnoreBatteryOptimizationsScreen(context: Context): Boolean { + return try { val intent = Intent( Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, "package:${context.packageName}".toUri() ) + // Add ONLY if non-Activity context (for best navigation UX) + if (context !is Activity) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + true + } else { + false + } + } catch (e: ActivityNotFoundException) { + Log_OC.w(TAG, "Direct battery optimization exemption screen not found", e) + false + } + } + + private fun tryOpenGeneralBatteryOptimizationSettings(context: Context): Boolean { + return try { + val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + + // Add ONLY if non-Activity context (for best navigation UX) + if (context !is Activity) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) + true } else { - // Fallback to generic battery optimization settings - context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) + false } - } catch (e: Exception) { - Log_OC.d(TAG, "open battery optimization settings: ", e) + } catch (e: ActivityNotFoundException) { + Log_OC.e(TAG, "General battery optimization settings screen not found", e) + false } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index c12acc61e2c5..ad5d6c9d333a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -849,22 +849,38 @@ class SyncedFoldersActivity : } private fun showBatteryOptimizationDialog() { + // Only show dialog if activity is in resumed state (prevents crashes) if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { Log_OC.w(TAG, "Activity not resumed, skipping battery dialog") return } + // Build the dialog with NO automatic positive button listener val dialog = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) .setTitle(R.string.battery_optimization_title) .setMessage(R.string.battery_optimization_message) - .setPositiveButton(R.string.battery_optimization_disable) { _, _ -> - BatteryOptimizationHelper.openBatteryOptimizationSettings(this) - } + .setPositiveButton(R.string.battery_optimization_disable, null) // Custom handler set below .setNeutralButton(R.string.battery_optimization_close, null) .setIcon(R.drawable.ic_battery_alert) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialog) + val alertDialog = dialog.show() + // Try to open battery optimization settings. If it fails, stay open for visibillity. + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val success = BatteryOptimizationHelper.openBatteryOptimizationSettings(this) + if (success) { + // We dismiss here because we successfully sent the user to Settings. + // If they come back without changing settings, the dialog stays gone + // until the next time showBatteryOptimizationDialogIfNeeded() runs. + alertDialog.dismiss() + } else { + showSnackMessage(getString(R.string.battery_optimization_unable_to_open_settings)) + } + } + + // Consistently style the dialog's buttons viewThemeUtils.platform.colorTextButtons( alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97f04a8862af..6e74bbfcc460 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1076,6 +1076,7 @@ Error retrieving templates Battery optimization Your device may have battery optimization enabled. AutoUpload works only properly if you exclude this app from it. + Unable to open battery optimization settings Disable Close Failed to load details