From cbd1803dc0c6e57924ac54b76dde84987a4dab6d Mon Sep 17 00:00:00 2001 From: devthejo Date: Fri, 5 Sep 2025 11:46:21 +0200 Subject: [PATCH] fix(android): battery optimisation --- android/app/src/main/AndroidManifest.xml | 2 + src/containers/PermissionWizard/HeroMode.js | 79 +++++++++++------ src/lib/native/batteryOptimization.js | 97 +++++++++++++++++++++ src/scenes/Params/Permissions.js | 69 ++++++++------- 4 files changed, 190 insertions(+), 57 deletions(-) create mode 100644 src/lib/native/batteryOptimization.js diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d3ecee3..dc7f7ed 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + @@ -10,6 +11,7 @@ + diff --git a/src/containers/PermissionWizard/HeroMode.js b/src/containers/PermissionWizard/HeroMode.js index 13bf4e2..2443bc3 100644 --- a/src/containers/PermissionWizard/HeroMode.js +++ b/src/containers/PermissionWizard/HeroMode.js @@ -17,11 +17,13 @@ import { import { createStyles, useTheme } from "~/theme"; import openSettings from "~/lib/native/openSettings"; import { - RequestDisableOptimization, - BatteryOptEnabled, -} from "react-native-battery-optimization-check"; + requestBatteryOptimizationExemption, + isBatteryOptimizationEnabled, + openBatteryOptimizationFallbacks, +} from "~/lib/native/batteryOptimization"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; +import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground"; import requestPermissionMotion from "~/permissions/requestPermissionMotion"; import CustomButton from "~/components/CustomButton"; import Text from "~/components/Text"; @@ -45,6 +47,8 @@ const HeroMode = () => { useState(null); const [batteryOptAttempted, setBatteryOptAttempted] = useState(false); const [batteryOptInProgress, setBatteryOptInProgress] = useState(false); + const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] = + useState(false); const permissions = usePermissionsState([ "locationBackground", "motion", @@ -75,16 +79,14 @@ const HeroMode = () => { setBatteryOptInProgress(true); // Check if battery optimization is enabled - const isEnabled = await BatteryOptEnabled(); + const isEnabled = await isBatteryOptimizationEnabled(); setBatteryOptimizationEnabled(isEnabled); if (isEnabled) { - console.log( - "Battery optimization is enabled, requesting to disable...", - ); + console.log("Battery optimization is enabled, requesting exemption..."); // Request to disable battery optimization (opens Android Settings) - RequestDisableOptimization(); + await requestBatteryOptimizationExemption(); setBatteryOptAttempted(true); // Return false to indicate user needs to complete action in Settings @@ -106,31 +108,45 @@ const HeroMode = () => { const handleRequestPermissions = useCallback(async () => { setRequesting(true); try { - // Don't change step immediately to avoid race conditions console.log("Starting permission requests..."); - // Request battery optimization FIRST (opens Android Settings) - // This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs + // 1) Battery optimization (opens Settings) const batteryOptDisabled = await handleBatteryOptimization(); console.log("Battery optimization disabled:", batteryOptDisabled); + if (!batteryOptDisabled) { + // Settings flow opened; wait for user to return before requesting in-app permissions + setRequesting(false); + setHasAttempted(true); + return; + } - // Request motion permission second + // 2) Foreground location + let fgGranted = await requestPermissionLocationForeground(); + permissionsActions.setLocationForeground(fgGranted); + console.log("Location foreground permission:", fgGranted); + + // 3) Background location (only after FG granted) + let bgGranted = false; + if (fgGranted) { + bgGranted = await requestPermissionLocationBackground(); + permissionsActions.setLocationBackground(bgGranted); + } else { + console.log( + "Skipping background location since foreground not granted", + ); + } + console.log("Location background permission:", bgGranted); + + // 4) Motion const motionGranted = await requestPermissionMotion.requestPermission(); permissionsActions.setMotion(motionGranted); console.log("Motion permission:", motionGranted); - // Request background location last (after user returns from Settings if needed) - const locationGranted = await requestPermissionLocationBackground(); - permissionsActions.setLocationBackground(locationGranted); - console.log("Location background permission:", locationGranted); - - // Only set step to tracking after all permission requests are complete + // Step after all requests permissionWizardActions.setCurrentStep("tracking"); - // Check if we should proceed to success immediately - if (locationGranted && motionGranted && batteryOptDisabled) { + if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) { permissionWizardActions.setHeroPermissionsGranted(true); - // Don't navigate immediately, let the useEffect handle it } } catch (error) { console.error("Error requesting permissions:", error); @@ -143,7 +159,7 @@ const HeroMode = () => { // Re-check battery optimization status before retrying if (Platform.OS === "android") { try { - const isEnabled = await BatteryOptEnabled(); + const isEnabled = await isBatteryOptimizationEnabled(); setBatteryOptimizationEnabled(isEnabled); // If battery optimization is now disabled, update the store @@ -179,7 +195,7 @@ const HeroMode = () => { const checkInitialBatteryOptimization = async () => { if (Platform.OS === "android") { try { - const isEnabled = await BatteryOptEnabled(); + const isEnabled = await isBatteryOptimizationEnabled(); setBatteryOptimizationEnabled(isEnabled); // If already disabled, update the store @@ -208,7 +224,7 @@ const HeroMode = () => { ) { console.log("App became active, re-checking battery optimization..."); try { - const isEnabled = await BatteryOptEnabled(); + const isEnabled = await isBatteryOptimizationEnabled(); setBatteryOptimizationEnabled(isEnabled); if (!isEnabled) { @@ -216,6 +232,19 @@ const HeroMode = () => { "Battery optimization disabled after returning from settings", ); permissionsActions.setBatteryOptimizationDisabled(true); + } else if (!batteryOptFallbackOpened) { + try { + console.log( + "Battery optimization still enabled; opening fallback settings...", + ); + await openBatteryOptimizationFallbacks(); + setBatteryOptFallbackOpened(true); + } catch (e) { + console.error( + "Error opening battery optimization fallback settings:", + e, + ); + } } } catch (error) { console.error( @@ -234,7 +263,7 @@ const HeroMode = () => { return () => { subscription?.remove(); }; - }, [batteryOptAttempted]); + }, [batteryOptAttempted, batteryOptFallbackOpened]); useEffect(() => { if (hasAttempted && allGranted) { diff --git a/src/lib/native/batteryOptimization.js b/src/lib/native/batteryOptimization.js new file mode 100644 index 0000000..7db6590 --- /dev/null +++ b/src/lib/native/batteryOptimization.js @@ -0,0 +1,97 @@ +import { Platform } from "react-native"; +import SendIntentAndroid from "react-native-send-intent"; +import { + RequestDisableOptimization, + BatteryOptEnabled, +} from "react-native-battery-optimization-check"; +import { createLogger } from "~/lib/logger"; +import { FEATURE_SCOPES } from "~/lib/logger/scopes"; + +const log = createLogger({ + module: FEATURE_SCOPES.PERMISSIONS, + feature: "battery-optimization", +}); + +/** + * Returns true if battery optimization is currently ENABLED for this app on Android. + * On iOS, returns false (no battery optimization concept). + */ +export async function isBatteryOptimizationEnabled() { + if (Platform.OS !== "android") return false; + try { + const enabled = await BatteryOptEnabled(); + log.info("Battery optimization status", { enabled }); + return enabled; + } catch (e) { + log.error("Failed to read battery optimization status", { + error: e?.message, + stack: e?.stack, + }); + // Conservative: assume enabled if unknown + return true; + } +} + +/** + * Launches the primary system flow to request ignoring battery optimizations. + * This opens a Settings screen; it does not yield a synchronous result. + * + * Returns: + * - false on Android to indicate the user must complete an action in Settings + * - true on iOS (no-op) + */ +export async function requestBatteryOptimizationExemption() { + if (Platform.OS !== "android") return true; + + try { + log.info("Requesting to disable battery optimization (primary intent)"); + // This opens the OS dialog/settings. No result is provided, handle via AppState return. + RequestDisableOptimization(); + return false; + } catch (e) { + log.error("Primary request to disable battery optimization failed", { + error: e?.message, + stack: e?.stack, + }); + // Even if it throws, we'll guide users via fallbacks. + return false; + } +} + +/** + * Opens best-effort fallback screens to let users disable battery optimization. + * Call this AFTER the user returns and status is still enabled. + * + * Strategy: + * - Try the list of battery optimization exceptions + * - Fallback to app settings + */ +export async function openBatteryOptimizationFallbacks() { + if (Platform.OS !== "android") return true; + + // Try the generic battery optimization settings list + try { + log.info("Opening fallback: IGNORE_BATTERY_OPTIMIZATION_SETTINGS"); + await SendIntentAndroid.openSettings( + "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS", + ); + return true; + } catch (e) { + log.warn("Failed to open IGNORE_BATTERY_OPTIMIZATION_SETTINGS", { + error: e?.message, + }); + } + + // Final fallback: app details settings + try { + log.info("Opening fallback: APPLICATION_DETAILS_SETTINGS (app details)"); + await SendIntentAndroid.openAppSettings(); + return true; + } catch (e) { + log.error("Failed to open APPLICATION_DETAILS_SETTINGS", { + error: e?.message, + stack: e?.stack, + }); + return false; + } +} diff --git a/src/scenes/Params/Permissions.js b/src/scenes/Params/Permissions.js index 257edfa..d542f1b 100644 --- a/src/scenes/Params/Permissions.js +++ b/src/scenes/Params/Permissions.js @@ -10,9 +10,9 @@ import { Button, Title } from "react-native-paper"; import { usePermissionsState, permissionsActions } from "~/stores"; import { Ionicons } from "@expo/vector-icons"; import { - RequestDisableOptimization, - BatteryOptEnabled, -} from "react-native-battery-optimization-check"; + requestBatteryOptimizationExemption, + isBatteryOptimizationEnabled, +} from "~/lib/native/batteryOptimization"; import openSettings from "~/lib/native/openSettings"; import requestPermissionFcm from "~/permissions/requestPermissionFcm"; @@ -26,23 +26,19 @@ import * as Location from "expo-location"; import * as Notifications from "expo-notifications"; import * as Contacts from "expo-contacts"; -// Battery optimization request handler const requestBatteryOptimizationDisable = async () => { - if (Platform.OS !== "android") { - return true; // iOS doesn't have battery optimization - } + if (Platform.OS !== "android") return true; try { - const isEnabled = await BatteryOptEnabled(); - if (isEnabled) { - console.log("Battery optimization enabled, requesting to disable..."); - RequestDisableOptimization(); - // Return false as the user needs to interact with the system dialog + const enabled = await isBatteryOptimizationEnabled(); + if (enabled) { + console.log("Battery optimization enabled, requesting exemption..."); + await requestBatteryOptimizationExemption(); + // User must interact in Settings; will re-check on AppState 'active' return false; - } else { - console.log("Battery optimization already disabled"); - return true; } + console.log("Battery optimization already disabled"); + return true; } catch (error) { console.error("Error handling battery optimization:", error); return false; @@ -110,8 +106,8 @@ const checkPermissionStatus = async (permission) => { return true; // iOS doesn't have battery optimization } try { - const isEnabled = await BatteryOptEnabled(); - return !isEnabled; // Return true if optimization is disabled + const enabled = await isBatteryOptimizationEnabled(); + return !enabled; // true if optimization is disabled } catch (error) { console.error("Error checking battery optimization:", error); return false; @@ -200,24 +196,33 @@ export default function Permissions() { const handleRequestPermission = async (permission) => { try { - const granted = await requestPermissions[permission](); - setPermissions[permission](granted); + let granted = false; - // For battery optimization, we need to handle the async nature differently - if ( - permission === "batteryOptimizationDisabled" && - Platform.OS === "android" - ) { - // Give a short delay for the system dialog to potentially complete - setTimeout(async () => { - const actualStatus = await checkPermissionStatus(permission); - setPermissions[permission](actualStatus); - }, 1000); + if (permission === "locationBackground") { + // Ensure foreground location is granted first + const fgGranted = await checkPermissionStatus("locationForeground"); + if (!fgGranted) { + const fgReq = await requestPermissionLocationForeground(); + setPermissions.locationForeground(fgReq); + if (!fgReq) { + granted = false; + } else { + granted = await requestPermissionLocationBackground(); + } + } else { + granted = await requestPermissionLocationBackground(); + } + setPermissions.locationBackground(granted); } else { - // Double-check the status to ensure UI is in sync - const actualStatus = await checkPermissionStatus(permission); - setPermissions[permission](actualStatus); + granted = await requestPermissions[permission](); + setPermissions[permission](granted); } + + // Double-check the status to ensure UI is in sync. + // For battery optimization, this immediate check may still be 'enabled'; + // we'll re-check again on AppState 'active' after returning from Settings. + const actualStatus = await checkPermissionStatus(permission); + setPermissions[permission](actualStatus); } catch (error) { console.error(`Error requesting ${permission} permission:`, error); }