Add ignore battery optimization check for Android clipboard clear (#1157)

This commit is contained in:
Leendert de Borst
2025-08-29 16:39:02 +02:00
committed by Leendert de Borst
parent 056f8e97e9
commit ab740c093f
6 changed files with 89 additions and 35 deletions

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>

View File

@@ -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()

View File

@@ -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

View File

@@ -27,7 +27,7 @@ export default function ClipboardClearScreen(): React.ReactNode {
const { t } = useTranslation();
const { getClipboardClearTimeout, setClipboardClearTimeout } = useAuth();
const [selectedTimeout, setSelectedTimeout] = useState<number>(10);
const [canScheduleExactAlarms, setCanScheduleExactAlarms] = useState<boolean>(true);
const [isIgnoringBatteryOptimizations, setIsIgnoringBatteryOptimizations] = useState<boolean>(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<void> => {
const checkBatteryOptimization = async (): Promise<void> => {
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<void> => {
const handleRequestBatteryOptimizationExemption = async (): Promise<void> => {
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 {
</ThemedText>
</View>
)}
{Platform.OS === 'android' && !canScheduleExactAlarms && selectedTimeout > 0 && (
{Platform.OS === 'android' && !isIgnoringBatteryOptimizations && selectedTimeout > 0 && (
<View style={styles.helpContainer}>
<ThemedText style={styles.helpTitle}>
{t('settings.exactAlarmHelpTitle')}
{t('settings.batteryOptimizationHelpTitle')}
</ThemedText>
<View style={styles.permissionStatusContainer}>
<Ionicons
name="alert-circle"
name="warning"
size={16}
color={colors.destructive || '#EF4444'}
/>
<ThemedText style={[styles.permissionStatusText, styles.permissionDenied]}>
{t('settings.exactAlarmPermissionDenied')}
{t('settings.batteryOptimizationActive')}
</ThemedText>
</View>
<ThemedText style={styles.helpDescription}>
{t('settings.exactAlarmHelpDescription')}
{t('settings.batteryOptimizationHelpDescription')}
</ThemedText>
<TouchableOpacity
style={styles.helpButton}
onPress={handleRequestExactAlarmPermission}
onPress={handleRequestBatteryOptimizationExemption}
>
<Ionicons name="settings" size={16} color={colors.background} />
<Ionicons name="battery-charging" size={16} color={colors.background} />
<ThemedText style={styles.helpButtonText}>
{t('settings.enableExactAlarms')}
{t('settings.disableBatteryOptimization')}
</ThemedText>
</TouchableOpacity>
</View>
)}
{Platform.OS === 'android' && canScheduleExactAlarms && selectedTimeout > 0 && (
{Platform.OS === 'android' && isIgnoringBatteryOptimizations && selectedTimeout > 0 && (
<View style={styles.helpContainer}>
<View style={styles.permissionStatusContainer}>
<Ionicons
@@ -255,12 +254,9 @@ export default function ClipboardClearScreen(): React.ReactNode {
color={colors.success || '#10B981'}
/>
<ThemedText style={[styles.permissionStatusText, styles.permissionGranted]}>
{t('settings.exactAlarmPermissionGranted')}
{t('settings.batteryOptimizationDisabled')}
</ThemedText>
</View>
<ThemedText style={styles.helpDescription}>
{t('settings.exactAlarmEnabledDescription')}
</ThemedText>
</View>
)}
</ThemedScrollView>

View File

@@ -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}})",

View File

@@ -45,6 +45,10 @@ export interface Spec extends TurboModule {
// Exact alarm permission management
canScheduleExactAlarms(): Promise<boolean>;
requestExactAlarmPermission(): Promise<string>;
// Battery optimization management
isIgnoringBatteryOptimizations(): Promise<boolean>;
requestIgnoreBatteryOptimizations(): Promise<string>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeVaultManager');