From ab740c093fbba41ff3640b139fdef8b33a2edc9d Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:39:02 +0200 Subject: [PATCH] Add ignore battery optimization check for Android clipboard clear (#1157) --- .../android/app/src/main/AndroidManifest.xml | 1 + .../ClipboardClearReceiver.kt | 1 - .../nativevaultmanager/NativeVaultManager.kt | 55 +++++++++++++++++++ .../app/(tabs)/settings/clipboard-clear.tsx | 52 ++++++++---------- apps/mobile-app/i18n/locales/en.json | 11 ++-- apps/mobile-app/specs/NativeVaultManager.ts | 4 ++ 6 files changed, 89 insertions(+), 35 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/AndroidManifest.xml b/apps/mobile-app/android/app/src/main/AndroidManifest.xml index 6bfee34f3..6657e225b 100644 --- a/apps/mobile-app/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile-app/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt index 717f0429b..eb9c84f93 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt @@ -19,7 +19,6 @@ class ClipboardClearReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { try { Log.d(TAG, "Received broadcast to clear clipboard") - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.clearPrimaryClip() diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt index e3ce85986..674380ae6 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt @@ -2,6 +2,7 @@ package net.aliasvault.app.nativevaultmanager import android.content.Intent import android.net.Uri +import android.os.PowerManager import android.provider.Settings import android.util.Log import androidx.core.net.toUri @@ -701,6 +702,60 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : } } + /** + * Check if the app is ignoring battery optimizations. + * @param promise The promise to resolve with boolean result + */ + @ReactMethod + override fun isIgnoringBatteryOptimizations(promise: Promise?) { + try { + val isIgnoring = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + val powerManager = reactApplicationContext.getSystemService(android.content.Context.POWER_SERVICE) as PowerManager + powerManager.isIgnoringBatteryOptimizations(reactApplicationContext.packageName) + } else { + true // Pre-Android 6.0 doesn't have battery optimization + } + Log.d(TAG, "Is ignoring battery optimizations: $isIgnoring") + promise?.resolve(isIgnoring) + } catch (e: Exception) { + Log.e(TAG, "Error checking battery optimization status", e) + promise?.reject("ERR_BATTERY_OPTIMIZATION_CHECK", "Failed to check battery optimization status: ${e.message}", e) + } + } + + /** + * Request battery optimization exemption by opening system settings. + * @param promise The promise to resolve + */ + @ReactMethod + override fun requestIgnoreBatteryOptimizations(promise: Promise?) { + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + val powerManager = reactApplicationContext.getSystemService(android.content.Context.POWER_SERVICE) as PowerManager + + if (!powerManager.isIgnoringBatteryOptimizations(reactApplicationContext.packageName)) { + Log.d(TAG, "Requesting battery optimization exemption via system settings") + val intent = Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${reactApplicationContext.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + reactApplicationContext.startActivity(intent) + promise?.resolve("Battery optimization exemption request sent - user will be taken to settings") + } else { + Log.d(TAG, "App is already ignoring battery optimizations") + promise?.resolve("App is already ignoring battery optimizations") + } + } else { + Log.d(TAG, "Battery optimization not applicable on this Android version") + promise?.resolve("Battery optimization not applicable on this Android version") + } + } catch (e: Exception) { + Log.e(TAG, "Error requesting battery optimization exemption", e) + promise?.reject("ERR_BATTERY_OPTIMIZATION_REQUEST", "Failed to request battery optimization exemption: ${e.message}", e) + } + } + /** * Get the auth methods. * @param promise The promise to resolve diff --git a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx index 25b1887d3..75d37190e 100644 --- a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx +++ b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx @@ -27,7 +27,7 @@ export default function ClipboardClearScreen(): React.ReactNode { const { t } = useTranslation(); const { getClipboardClearTimeout, setClipboardClearTimeout } = useAuth(); const [selectedTimeout, setSelectedTimeout] = useState(10); - const [canScheduleExactAlarms, setCanScheduleExactAlarms] = useState(true); + const [isIgnoringBatteryOptimizations, setIsIgnoringBatteryOptimizations] = useState(true); const appState = useRef(AppState.currentState); useEffect(() => { @@ -40,30 +40,30 @@ export default function ClipboardClearScreen(): React.ReactNode { }; /** - * Check exact alarm permission status on Android. + * Check battery optimization exemption status on Android. */ - const checkExactAlarmPermission = async (): Promise => { + const checkBatteryOptimization = async (): Promise => { if (Platform.OS === 'android') { try { - const canSchedule = await NativeVaultManager.canScheduleExactAlarms(); - setCanScheduleExactAlarms(canSchedule); + const isIgnoring = await NativeVaultManager.isIgnoringBatteryOptimizations(); + setIsIgnoringBatteryOptimizations(isIgnoring); } catch (error) { - console.error('Error checking exact alarm permission:', error); + console.error('Error checking battery optimization status:', error); // Default to true to avoid showing the help section unnecessarily - setCanScheduleExactAlarms(true); + setIsIgnoringBatteryOptimizations(true); } } }; loadCurrentTimeout(); - checkExactAlarmPermission(); + checkBatteryOptimization(); // Listen for app state changes to re-check permission when app comes to foreground const subscription = AppState.addEventListener('change', async (nextAppState) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { - // App coming to foreground, re-check exact alarm permission + // App coming to foreground, re-check battery optimization if (Platform.OS === 'android') { - await checkExactAlarmPermission(); + await checkBatteryOptimization(); } } appState.current = nextAppState; @@ -83,17 +83,17 @@ export default function ClipboardClearScreen(): React.ReactNode { }; /** - * Handle exact alarm permission request. + * Handle battery optimization exemption request. */ - const handleRequestExactAlarmPermission = async (): Promise => { + const handleRequestBatteryOptimizationExemption = async (): Promise => { if (Platform.OS !== 'android') { return; } try { - await NativeVaultManager.requestExactAlarmPermission(); + await NativeVaultManager.requestIgnoreBatteryOptimizations(); } catch (error) { - console.error('Error handling exact alarm permission request:', error); + console.error('Error handling battery optimization exemption request:', error); } }; @@ -167,7 +167,6 @@ export default function ClipboardClearScreen(): React.ReactNode { permissionStatusContainer: { alignItems: 'center', flexDirection: 'row', - marginBottom: 12, }, permissionStatusText: { fontSize: 13, @@ -217,36 +216,36 @@ export default function ClipboardClearScreen(): React.ReactNode { )} - {Platform.OS === 'android' && !canScheduleExactAlarms && selectedTimeout > 0 && ( + {Platform.OS === 'android' && !isIgnoringBatteryOptimizations && selectedTimeout > 0 && ( - {t('settings.exactAlarmHelpTitle')} + {t('settings.batteryOptimizationHelpTitle')} - {t('settings.exactAlarmPermissionDenied')} + {t('settings.batteryOptimizationActive')} - {t('settings.exactAlarmHelpDescription')} + {t('settings.batteryOptimizationHelpDescription')} - + - {t('settings.enableExactAlarms')} + {t('settings.disableBatteryOptimization')} )} - {Platform.OS === 'android' && canScheduleExactAlarms && selectedTimeout > 0 && ( + {Platform.OS === 'android' && isIgnoringBatteryOptimizations && selectedTimeout > 0 && ( - {t('settings.exactAlarmPermissionGranted')} + {t('settings.batteryOptimizationDisabled')} - - {t('settings.exactAlarmEnabledDescription')} - )} diff --git a/apps/mobile-app/i18n/locales/en.json b/apps/mobile-app/i18n/locales/en.json index d8ef50fb2..b3ec58d8c 100644 --- a/apps/mobile-app/i18n/locales/en.json +++ b/apps/mobile-app/i18n/locales/en.json @@ -213,12 +213,11 @@ "15seconds": "15 seconds", "30seconds": "30 seconds" }, - "exactAlarmHelpTitle": "Improve Clipboard Clearing Accuracy", - "exactAlarmPermissionDenied": "Missing permission", - "exactAlarmPermissionGranted": "Permission granted", - "exactAlarmHelpDescription": "Some Android devices require special permission for precise background clipboard clearing. Without this permission, AliasVault uses approximate timing which may be less reliable when the app is in the background.", - "exactAlarmEnabledDescription": "AliasVault can clear your clipboard with precise timing, even when the app is in the background.", - "enableExactAlarms": "Open app settings", + "batteryOptimizationHelpTitle": "Enable Background Clipboard Clearing", + "batteryOptimizationActive": "Battery optimization is blocking background tasks", + "batteryOptimizationDisabled": "Background clipboard clearing enabled", + "batteryOptimizationHelpDescription": "Android's battery optimization prevents reliable clipboard clearing when the app is in the background. Disabling battery optimization for AliasVault allows precise background clipboard clearing and automatically grants necessary alarm permissions.", + "disableBatteryOptimization": "Disable battery optimization", "identityGenerator": "Identity Generator", "security": "Security", "appVersion": "App version {{version}} ({{url}})", diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index 4ee5b3f2c..421f1e10c 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -45,6 +45,10 @@ export interface Spec extends TurboModule { // Exact alarm permission management canScheduleExactAlarms(): Promise; requestExactAlarmPermission(): Promise; + + // Battery optimization management + isIgnoringBatteryOptimizations(): Promise; + requestIgnoreBatteryOptimizations(): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager');