Compare commits
3 commits
41bb6fcd2d
...
b21a91bb9e
| Author | SHA1 | Date | |
|---|---|---|---|
| b21a91bb9e | |||
| c7d0b36f1b | |||
| 6c8153bdb1 |
7 changed files with 153 additions and 30 deletions
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
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.16.6](https://github.com/alerte-secours/as-app/compare/v1.16.5...v1.16.6) (2026-01-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* permissions screen ([6c8153b](https://github.com/alerte-secours/as-app/commit/6c8153bdb114582f9a597a50fd2604f7b90dd181))
|
||||||
|
* **track-location:** try 3 ([41bb6fc](https://github.com/alerte-secours/as-app/commit/41bb6fcd2dc0999d212ceab0c9a6b6b64fe07cce))
|
||||||
|
* **track-location:** try 4 ([c7d0b36](https://github.com/alerte-secours/as-app/commit/c7d0b36f1bd3e700ad15bbc3ecff987e61aadbd9))
|
||||||
|
|
||||||
## [1.16.5](https://github.com/alerte-secours/as-app/compare/v1.16.4...v1.16.5) (2026-01-20)
|
## [1.16.5](https://github.com/alerte-secours/as-app/compare/v1.16.4...v1.16.5) (2026-01-20)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 236
|
versionCode 237
|
||||||
versionName "1.16.5"
|
versionName "1.16.6"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
|
||||||
|
|
@ -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.16.5</string>
|
<string>1.16.6</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>236</string>
|
<string>237</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alerte-secours",
|
"name": "alerte-secours",
|
||||||
"version": "1.16.5",
|
"version": "1.16.6",
|
||||||
"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",
|
||||||
|
|
@ -53,8 +53,8 @@
|
||||||
"screenshot:android": "scripts/screenshot-android.sh"
|
"screenshot:android": "scripts/screenshot-android.sh"
|
||||||
},
|
},
|
||||||
"customExpoVersioning": {
|
"customExpoVersioning": {
|
||||||
"versionCode": 236,
|
"versionCode": 237,
|
||||||
"buildNumber": 236
|
"buildNumber": 237
|
||||||
},
|
},
|
||||||
"commit-and-tag-version": {
|
"commit-and-tag-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,10 @@ export const BASE_GEOLOCATION_CONFIG = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
httpRootProperty: "location",
|
httpRootProperty: "location",
|
||||||
batchSync: false,
|
batchSync: false,
|
||||||
autoSync: true,
|
// We intentionally disable autoSync and perform controlled uploads from explicit triggers
|
||||||
|
// (startup, identity-change, moving-edge, active-alert). This prevents stationary "ghost"
|
||||||
|
// uploads from low-quality locations produced by some Android devices.
|
||||||
|
autoSync: false,
|
||||||
|
|
||||||
// Persistence: keep enough records for offline catch-up.
|
// Persistence: keep enough records for offline catch-up.
|
||||||
// (The SDK already constrains with maxDaysToPersist; records are deleted after successful upload.)
|
// (The SDK already constrains with maxDaysToPersist; records are deleted after successful upload.)
|
||||||
|
|
@ -101,6 +104,7 @@ export const BASE_GEOLOCATION_INVARIANTS = {
|
||||||
speedJumpFilter: 100,
|
speedJumpFilter: 100,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
httpRootProperty: "location",
|
httpRootProperty: "location",
|
||||||
|
autoSync: false,
|
||||||
maxRecordsToPersist: 1000,
|
maxRecordsToPersist: 1000,
|
||||||
maxDaysToPersist: 7,
|
maxDaysToPersist: 7,
|
||||||
};
|
};
|
||||||
|
|
@ -113,10 +117,9 @@ export const TRACKING_PROFILES = {
|
||||||
// only periodic fixes (several times/hour). Note many config options like
|
// only periodic fixes (several times/hour). Note many config options like
|
||||||
// `distanceFilter` / `stationaryRadius` are documented as having little/no
|
// `distanceFilter` / `stationaryRadius` are documented as having little/no
|
||||||
// effect in this mode.
|
// effect in this mode.
|
||||||
// Some iOS devices / user settings can result in unreliable significant-change wakeups.
|
// Some devices / OEMs can be unreliable with significant-change only.
|
||||||
// We keep SLC for Android (battery), but fall back to standard motion tracking on iOS
|
// Use standard motion tracking for reliability, with conservative distanceFilter.
|
||||||
// with a conservative distanceFilter.
|
useSignificantChangesOnly: false,
|
||||||
useSignificantChangesOnly: Platform.OS !== "ios",
|
|
||||||
|
|
||||||
// Defensive: if some devices/platform conditions fall back to standard tracking,
|
// Defensive: if some devices/platform conditions fall back to standard tracking,
|
||||||
// keep the distanceFilter conservative to avoid battery drain.
|
// keep the distanceFilter conservative to avoid battery drain.
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,46 @@ export default function trackLocation() {
|
||||||
let didDisableUploadsForAnonymous = false;
|
let didDisableUploadsForAnonymous = false;
|
||||||
let didSyncAfterAuth = false;
|
let didSyncAfterAuth = false;
|
||||||
let didSyncAfterStartupFix = false;
|
let didSyncAfterStartupFix = false;
|
||||||
|
let lastMovingEdgeAt = 0;
|
||||||
|
const MOVING_EDGE_COOLDOWN_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
const BAD_ACCURACY_THRESHOLD_M = 200;
|
||||||
|
|
||||||
// Track identity so we can force a first geopoint when the effective user changes.
|
// Track identity so we can force a first geopoint when the effective user changes.
|
||||||
let lastSessionUserId = null;
|
let lastSessionUserId = null;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const pruneBadLocations = async () => {
|
||||||
|
try {
|
||||||
|
const locations = await BackgroundGeolocation.getLocations();
|
||||||
|
if (!Array.isArray(locations) || !locations.length) return 0;
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
// Defensive: only scan a bounded amount to avoid heavy work.
|
||||||
|
const toScan = locations.slice(-200);
|
||||||
|
for (const loc of toScan) {
|
||||||
|
const acc = loc?.coords?.accuracy;
|
||||||
|
const uuid = loc?.uuid;
|
||||||
|
if (
|
||||||
|
typeof acc === "number" &&
|
||||||
|
acc > BAD_ACCURACY_THRESHOLD_M &&
|
||||||
|
uuid
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.destroyLocation(uuid);
|
||||||
|
deleted++;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const safeSync = async (reason) => {
|
const safeSync = async (reason) => {
|
||||||
// Sync can fail transiently (SDK busy, network warming up, etc). Retry a few times.
|
// Sync can fail transiently (SDK busy, network warming up, etc). Retry a few times.
|
||||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
|
@ -65,6 +99,8 @@ export default function trackLocation() {
|
||||||
BackgroundGeolocation.getCount(),
|
BackgroundGeolocation.getCount(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const pruned = await pruneBadLocations();
|
||||||
|
|
||||||
locationLogger.info("Attempting BGGeo sync", {
|
locationLogger.info("Attempting BGGeo sync", {
|
||||||
reason,
|
reason,
|
||||||
attempt,
|
attempt,
|
||||||
|
|
@ -72,6 +108,7 @@ export default function trackLocation() {
|
||||||
isMoving: state?.isMoving,
|
isMoving: state?.isMoving,
|
||||||
trackingMode: state?.trackingMode,
|
trackingMode: state?.trackingMode,
|
||||||
pendingBefore,
|
pendingBefore,
|
||||||
|
pruned,
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await BackgroundGeolocation.sync();
|
const records = await BackgroundGeolocation.sync();
|
||||||
|
|
@ -421,7 +458,7 @@ export default function trackLocation() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
currentSessionUserId = null;
|
currentSessionUserId = null;
|
||||||
}
|
}
|
||||||
if (!userToken) {
|
if (!userToken && !currentSessionUserId) {
|
||||||
// Pre-login mode: keep tracking enabled but disable uploads.
|
// Pre-login mode: keep tracking enabled but disable uploads.
|
||||||
// Also applies to logout: keep tracking on (per product requirement: track all the time),
|
// Also applies to logout: keep tracking on (per product requirement: track all the time),
|
||||||
// but stop sending anything to server without auth.
|
// but stop sending anything to server without auth.
|
||||||
|
|
@ -491,7 +528,7 @@ export default function trackLocation() {
|
||||||
locationLogger.debug("Updating background geolocation config");
|
locationLogger.debug("Updating background geolocation config");
|
||||||
await BackgroundGeolocation.setConfig({
|
await BackgroundGeolocation.setConfig({
|
||||||
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
||||||
autoSync: true,
|
autoSync: false,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${userToken}`,
|
Authorization: `Bearer ${userToken}`,
|
||||||
},
|
},
|
||||||
|
|
@ -618,6 +655,30 @@ export default function trackLocation() {
|
||||||
// The final persisted location will arrive with sample=false.
|
// The final persisted location will arrive with sample=false.
|
||||||
if (location.sample) return;
|
if (location.sample) return;
|
||||||
|
|
||||||
|
// Quality gate: delete very poor-accuracy locations to prevent them from being synced
|
||||||
|
// and ignore them for UI/state.
|
||||||
|
const acc = location?.coords?.accuracy;
|
||||||
|
if (typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M) {
|
||||||
|
locationLogger.info("Ignoring poor-accuracy location", {
|
||||||
|
accuracy: acc,
|
||||||
|
uuid: location?.uuid,
|
||||||
|
});
|
||||||
|
if (location?.uuid) {
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.destroyLocation(location.uuid);
|
||||||
|
locationLogger.debug("Destroyed poor-accuracy location", {
|
||||||
|
uuid: location.uuid,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Failed to destroy poor-accuracy location", {
|
||||||
|
uuid: location?.uuid,
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
location.coords &&
|
location.coords &&
|
||||||
location.coords.latitude &&
|
location.coords.latitude &&
|
||||||
|
|
@ -681,6 +742,46 @@ export default function trackLocation() {
|
||||||
isMoving: event?.isMoving,
|
isMoving: event?.isMoving,
|
||||||
location: event?.location?.coords,
|
location: event?.location?.coords,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Moving-edge strategy: when we enter moving state, force one persisted high-quality
|
||||||
|
// point + sync so the server gets a quick update.
|
||||||
|
//
|
||||||
|
// IMPORTANT: Restrict this to ACTIVE tracking only. On Android, motion detection can
|
||||||
|
// produce false-positive moving transitions while the device is stationary (screen-off),
|
||||||
|
// which would otherwise trigger unwanted background uploads.
|
||||||
|
// Cooldown to avoid repeated work due to motion jitter.
|
||||||
|
if (event?.isMoving && authReady && currentProfile === "active") {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastMovingEdgeAt >= MOVING_EDGE_COOLDOWN_MS) {
|
||||||
|
lastMovingEdgeAt = now;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fix = await BackgroundGeolocation.getCurrentPosition({
|
||||||
|
samples: 1,
|
||||||
|
persist: true,
|
||||||
|
timeout: 30,
|
||||||
|
maximumAge: 0,
|
||||||
|
desiredAccuracy: 50,
|
||||||
|
extras: {
|
||||||
|
moving_edge: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
locationLogger.info("Moving-edge fix acquired", {
|
||||||
|
accuracy: fix?.coords?.accuracy,
|
||||||
|
latitude: fix?.coords?.latitude,
|
||||||
|
longitude: fix?.coords?.longitude,
|
||||||
|
timestamp: fix?.timestamp,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Moving-edge fix failed", {
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await safeSync("moving-edge");
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onActivityChange: (event) => {
|
onActivityChange: (event) => {
|
||||||
locationLogger.info("Activity change", {
|
locationLogger.info("Activity change", {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useCallback, useRef } from "react";
|
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
|
@ -309,8 +309,10 @@ const PermissionItem = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Permissions() {
|
export default function Permissions() {
|
||||||
// Create permissions list based on platform
|
// IMPORTANT: keep a stable permissions list across renders.
|
||||||
const getPermissionsList = () => {
|
// If this array changes identity each render, it re-creates callbacks and can
|
||||||
|
// trigger a re-render loop via permission checks + store updates.
|
||||||
|
const permissionsList = useMemo(() => {
|
||||||
const basePermissions = [
|
const basePermissions = [
|
||||||
"fcm",
|
"fcm",
|
||||||
"phoneCall",
|
"phoneCall",
|
||||||
|
|
@ -319,29 +321,31 @@ export default function Permissions() {
|
||||||
"motion",
|
"motion",
|
||||||
"readContacts",
|
"readContacts",
|
||||||
];
|
];
|
||||||
|
return Platform.OS === "android"
|
||||||
// Add battery optimization only on Android
|
? [...basePermissions, "batteryOptimizationDisabled"]
|
||||||
if (Platform.OS === "android") {
|
: basePermissions;
|
||||||
return [...basePermissions, "batteryOptimizationDisabled"];
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
return basePermissions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const permissionsList = getPermissionsList();
|
|
||||||
const permissionsState = usePermissionsState(permissionsList);
|
const permissionsState = usePermissionsState(permissionsList);
|
||||||
|
|
||||||
const titleRef = useRef(null);
|
const titleRef = useRef(null);
|
||||||
const lastAnnouncementRef = useRef({});
|
const lastAnnouncementRef = useRef({});
|
||||||
|
const lastSetPermissionRef = useRef({});
|
||||||
|
const lastBlockedMapRef = useRef(null);
|
||||||
|
|
||||||
// We keep a minimal, best-effort blocked map for a11y/UX.
|
// We keep a minimal, best-effort blocked map for a11y/UX.
|
||||||
const [blockedMap, setBlockedMap] = React.useState({});
|
const [blockedMap, setBlockedMap] = React.useState({});
|
||||||
|
|
||||||
// Memoize the check permissions function
|
// Memoize the check permissions function.
|
||||||
|
// NOTE: Do NOT depend on permissionsState here (it changes on each store update),
|
||||||
|
// or we can re-run effects and create a log spam loop.
|
||||||
const checkAllPermissions = useCallback(async () => {
|
const checkAllPermissions = useCallback(async () => {
|
||||||
|
// Update store only when values actually change, to avoid churn.
|
||||||
for (const permission of permissionsList) {
|
for (const permission of permissionsList) {
|
||||||
const status = await checkPermissionStatus(permission);
|
const status = await checkPermissionStatus(permission);
|
||||||
setPermissions[permission](status);
|
if (lastSetPermissionRef.current[permission] !== status) {
|
||||||
|
lastSetPermissionRef.current[permission] = status;
|
||||||
|
setPermissions[permission](status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also refresh "blocked" state used for a11y guidance.
|
// Also refresh "blocked" state used for a11y guidance.
|
||||||
|
|
@ -350,7 +354,13 @@ export default function Permissions() {
|
||||||
const meta = await getPermissionA11yMeta(permission);
|
const meta = await getPermissionA11yMeta(permission);
|
||||||
nextBlocked[permission] = !!meta.blocked;
|
nextBlocked[permission] = !!meta.blocked;
|
||||||
}
|
}
|
||||||
setBlockedMap(nextBlocked);
|
|
||||||
|
// Avoid re-setting state if unchanged (prevents extra re-renders).
|
||||||
|
const nextBlockedKey = JSON.stringify(nextBlocked);
|
||||||
|
if (lastBlockedMapRef.current !== nextBlockedKey) {
|
||||||
|
lastBlockedMapRef.current = nextBlockedKey;
|
||||||
|
setBlockedMap(nextBlocked);
|
||||||
|
}
|
||||||
}, [permissionsList]);
|
}, [permissionsList]);
|
||||||
|
|
||||||
// Check all permissions when component mounts
|
// Check all permissions when component mounts
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue