Compare commits

..

No commits in common. "main" and "v1.12.2" have entirely different histories.

8 changed files with 64 additions and 199 deletions

View file

@ -2,8 +2,6 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.12.3](https://github.com/alerte-secours/as-app/compare/v1.12.2...v1.12.3) (2025-09-05)
## [1.12.2](https://github.com/alerte-secours/as-app/compare/v1.12.1...v1.12.2) (2025-08-24) ## [1.12.2](https://github.com/alerte-secours/as-app/compare/v1.12.1...v1.12.2) (2025-08-24)

View file

@ -83,8 +83,8 @@ android {
applicationId 'com.alertesecours' applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 211 versionCode 210
versionName "1.12.3" versionName "1.12.2"
multiDexEnabled true multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -2,7 +2,6 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.CALL_PHONE"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
@ -11,7 +10,6 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>

View file

@ -25,7 +25,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.12.3</string> <string>1.12.2</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -48,7 +48,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>211</string> <string>210</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -1,6 +1,6 @@
{ {
"name": "alerte-secours", "name": "alerte-secours",
"version": "1.12.3", "version": "1.12.2",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem", "start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
@ -50,8 +50,8 @@
"screenshot:android": "scripts/screenshot-android.sh" "screenshot:android": "scripts/screenshot-android.sh"
}, },
"customExpoVersioning": { "customExpoVersioning": {
"versionCode": 211, "versionCode": 210,
"buildNumber": 211 "buildNumber": 210
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {

View file

@ -17,13 +17,11 @@ import {
import { createStyles, useTheme } from "~/theme"; import { createStyles, useTheme } from "~/theme";
import openSettings from "~/lib/native/openSettings"; import openSettings from "~/lib/native/openSettings";
import { import {
requestBatteryOptimizationExemption, RequestDisableOptimization,
isBatteryOptimizationEnabled, BatteryOptEnabled,
openBatteryOptimizationFallbacks, } from "react-native-battery-optimization-check";
} from "~/lib/native/batteryOptimization";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
import requestPermissionMotion from "~/permissions/requestPermissionMotion"; import requestPermissionMotion from "~/permissions/requestPermissionMotion";
import CustomButton from "~/components/CustomButton"; import CustomButton from "~/components/CustomButton";
import Text from "~/components/Text"; import Text from "~/components/Text";
@ -47,8 +45,6 @@ const HeroMode = () => {
useState(null); useState(null);
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false); const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false); const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] =
useState(false);
const permissions = usePermissionsState([ const permissions = usePermissionsState([
"locationBackground", "locationBackground",
"motion", "motion",
@ -79,14 +75,16 @@ const HeroMode = () => {
setBatteryOptInProgress(true); setBatteryOptInProgress(true);
// Check if battery optimization is enabled // Check if battery optimization is enabled
const isEnabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
if (isEnabled) { if (isEnabled) {
console.log("Battery optimization is enabled, requesting exemption..."); console.log(
"Battery optimization is enabled, requesting to disable...",
);
// Request to disable battery optimization (opens Android Settings) // Request to disable battery optimization (opens Android Settings)
await requestBatteryOptimizationExemption(); RequestDisableOptimization();
setBatteryOptAttempted(true); setBatteryOptAttempted(true);
// Return false to indicate user needs to complete action in Settings // Return false to indicate user needs to complete action in Settings
@ -108,45 +106,31 @@ const HeroMode = () => {
const handleRequestPermissions = useCallback(async () => { const handleRequestPermissions = useCallback(async () => {
setRequesting(true); setRequesting(true);
try { try {
// Don't change step immediately to avoid race conditions
console.log("Starting permission requests..."); console.log("Starting permission requests...");
// 1) Battery optimization (opens Settings) // Request battery optimization FIRST (opens Android Settings)
// This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs
const batteryOptDisabled = await handleBatteryOptimization(); const batteryOptDisabled = await handleBatteryOptimization();
console.log("Battery optimization disabled:", batteryOptDisabled); 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;
}
// 2) Foreground location // Request motion permission second
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(); const motionGranted = await requestPermissionMotion.requestPermission();
permissionsActions.setMotion(motionGranted); permissionsActions.setMotion(motionGranted);
console.log("Motion permission:", motionGranted); console.log("Motion permission:", motionGranted);
// Step after all requests // 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
permissionWizardActions.setCurrentStep("tracking"); permissionWizardActions.setCurrentStep("tracking");
if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) { // Check if we should proceed to success immediately
if (locationGranted && motionGranted && batteryOptDisabled) {
permissionWizardActions.setHeroPermissionsGranted(true); permissionWizardActions.setHeroPermissionsGranted(true);
// Don't navigate immediately, let the useEffect handle it
} }
} catch (error) { } catch (error) {
console.error("Error requesting permissions:", error); console.error("Error requesting permissions:", error);
@ -159,7 +143,7 @@ const HeroMode = () => {
// Re-check battery optimization status before retrying // Re-check battery optimization status before retrying
if (Platform.OS === "android") { if (Platform.OS === "android") {
try { try {
const isEnabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
// If battery optimization is now disabled, update the store // If battery optimization is now disabled, update the store
@ -195,7 +179,7 @@ const HeroMode = () => {
const checkInitialBatteryOptimization = async () => { const checkInitialBatteryOptimization = async () => {
if (Platform.OS === "android") { if (Platform.OS === "android") {
try { try {
const isEnabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
// If already disabled, update the store // If already disabled, update the store
@ -224,7 +208,7 @@ const HeroMode = () => {
) { ) {
console.log("App became active, re-checking battery optimization..."); console.log("App became active, re-checking battery optimization...");
try { try {
const isEnabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
if (!isEnabled) { if (!isEnabled) {
@ -232,19 +216,6 @@ const HeroMode = () => {
"Battery optimization disabled after returning from settings", "Battery optimization disabled after returning from settings",
); );
permissionsActions.setBatteryOptimizationDisabled(true); 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) { } catch (error) {
console.error( console.error(
@ -263,7 +234,7 @@ const HeroMode = () => {
return () => { return () => {
subscription?.remove(); subscription?.remove();
}; };
}, [batteryOptAttempted, batteryOptFallbackOpened]); }, [batteryOptAttempted]);
useEffect(() => { useEffect(() => {
if (hasAttempted && allGranted) { if (hasAttempted && allGranted) {

View file

@ -1,97 +0,0 @@
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;
}
}

View file

@ -10,9 +10,9 @@ import { Button, Title } from "react-native-paper";
import { usePermissionsState, permissionsActions } from "~/stores"; import { usePermissionsState, permissionsActions } from "~/stores";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
requestBatteryOptimizationExemption, RequestDisableOptimization,
isBatteryOptimizationEnabled, BatteryOptEnabled,
} from "~/lib/native/batteryOptimization"; } from "react-native-battery-optimization-check";
import openSettings from "~/lib/native/openSettings"; import openSettings from "~/lib/native/openSettings";
import requestPermissionFcm from "~/permissions/requestPermissionFcm"; import requestPermissionFcm from "~/permissions/requestPermissionFcm";
@ -26,19 +26,23 @@ import * as Location from "expo-location";
import * as Notifications from "expo-notifications"; import * as Notifications from "expo-notifications";
import * as Contacts from "expo-contacts"; import * as Contacts from "expo-contacts";
// Battery optimization request handler
const requestBatteryOptimizationDisable = async () => { const requestBatteryOptimizationDisable = async () => {
if (Platform.OS !== "android") return true; if (Platform.OS !== "android") {
return true; // iOS doesn't have battery optimization
}
try { try {
const enabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
if (enabled) { if (isEnabled) {
console.log("Battery optimization enabled, requesting exemption..."); console.log("Battery optimization enabled, requesting to disable...");
await requestBatteryOptimizationExemption(); RequestDisableOptimization();
// User must interact in Settings; will re-check on AppState 'active' // Return false as the user needs to interact with the system dialog
return false; return false;
} } else {
console.log("Battery optimization already disabled"); console.log("Battery optimization already disabled");
return true; return true;
}
} catch (error) { } catch (error) {
console.error("Error handling battery optimization:", error); console.error("Error handling battery optimization:", error);
return false; return false;
@ -106,8 +110,8 @@ const checkPermissionStatus = async (permission) => {
return true; // iOS doesn't have battery optimization return true; // iOS doesn't have battery optimization
} }
try { try {
const enabled = await isBatteryOptimizationEnabled(); const isEnabled = await BatteryOptEnabled();
return !enabled; // true if optimization is disabled return !isEnabled; // Return true if optimization is disabled
} catch (error) { } catch (error) {
console.error("Error checking battery optimization:", error); console.error("Error checking battery optimization:", error);
return false; return false;
@ -196,33 +200,24 @@ export default function Permissions() {
const handleRequestPermission = async (permission) => { const handleRequestPermission = async (permission) => {
try { try {
let granted = false; const granted = await requestPermissions[permission]();
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 {
granted = await requestPermissions[permission]();
setPermissions[permission](granted); setPermissions[permission](granted);
}
// Double-check the status to ensure UI is in sync. // For battery optimization, we need to handle the async nature differently
// For battery optimization, this immediate check may still be 'enabled'; if (
// we'll re-check again on AppState 'active' after returning from Settings. permission === "batteryOptimizationDisabled" &&
Platform.OS === "android"
) {
// Give a short delay for the system dialog to potentially complete
setTimeout(async () => {
const actualStatus = await checkPermissionStatus(permission); const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus); setPermissions[permission](actualStatus);
}, 1000);
} else {
// Double-check the status to ensure UI is in sync
const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus);
}
} catch (error) { } catch (error) {
console.error(`Error requesting ${permission} permission:`, error); console.error(`Error requesting ${permission} permission:`, error);
} }