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);
}