Geolocation: GrapheneOS-correct fix — parallel hi/lo watches, 60s, progress
The persistent failure on GrapheneOS (no Play Services, GPS-only) was caused by the acquisition strategy itself, not the OS: - The old low-accuracy-only watch never fired without a fused/network provider, and the 25s deadline killed it before a cold GPS lock (which takes 30–60s on GrapheneOS) could ever deliver. Rewrite raceForFirstFix to actually work on GPS-only devices: - Run BOTH a high-accuracy (GPS) and low-accuracy (network) watch in parallel — high accuracy is what engages the GPS chip and is usually the one that delivers on GrapheneOS. - Also fire a one-shot high-accuracy getCurrentPosition. - Extend the deadline to 60s (real cold-GPS lock time) and classify a timeout-flavored failure correctly. - Ignore invalid/null-island (0,0) readings. - Drop the redundant post-race extra attempt. UX: show a persistent "Locating you…" toast (dismissed on success/fail) so the up-to-60s GPS lock doesn't look frozen, plus a "Found you" confirmation. Failure still surfaces the raw error text for diagnosis. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
a3dad2cbec
commit
59a801d525
2 changed files with 79 additions and 41 deletions
|
|
@ -184,24 +184,37 @@ function raceForFirstFix(
|
|||
): Promise<GeoCoords> {
|
||||
return new Promise<GeoCoords>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let watchId: string | null = null;
|
||||
const watchIds: string[] = [];
|
||||
let deadline: ReturnType<typeof setTimeout> | null = null;
|
||||
let sawTimeoutish = false; // remember if the failure looked like a timeout
|
||||
|
||||
const cleanup = () => {
|
||||
if (deadline) clearTimeout(deadline);
|
||||
if (watchId !== null) {
|
||||
const id = watchId;
|
||||
watchId = null;
|
||||
// clearWatch is fire-and-forget; guard so it can't throw into us.
|
||||
const clearAllWatches = () => {
|
||||
while (watchIds.length) {
|
||||
const id = watchIds.pop()!;
|
||||
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (deadline) clearTimeout(deadline);
|
||||
clearAllWatches();
|
||||
};
|
||||
|
||||
const succeed = (pos: CapPosition) => {
|
||||
if (settled) return;
|
||||
// Guard against garbage readings (0,0 / null island).
|
||||
const { latitude, longitude } = pos?.coords ?? {};
|
||||
if (
|
||||
typeof latitude !== 'number' ||
|
||||
typeof longitude !== 'number' ||
|
||||
(latitude === 0 && longitude === 0)
|
||||
) {
|
||||
console.warn('[geolocation] ignoring invalid reading', pos);
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude });
|
||||
resolve({ lat: latitude, lng: longitude });
|
||||
};
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
|
|
@ -211,46 +224,67 @@ function raceForFirstFix(
|
|||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
};
|
||||
|
||||
// Overall deadline — if nothing arrives, let the caller try high-accuracy.
|
||||
deadline = setTimeout(() => fail(new Error('fast-fix timeout')), 25000);
|
||||
// GrapheneOS (no Play Services) has no fast fused provider — a cold GPS lock
|
||||
// can genuinely take 30–60s. Give it a real chance before giving up.
|
||||
deadline = setTimeout(
|
||||
() => fail(new Error(sawTimeoutish ? 'timeout' : 'fast-fix timeout')),
|
||||
60000,
|
||||
);
|
||||
|
||||
// 1) watchPosition — first update wins (fast network/fused fix).
|
||||
if (typeof native.watchPosition === 'function') {
|
||||
// Register a watch with the given accuracy; first valid reading wins.
|
||||
const startWatch = (highAccuracy: boolean) => {
|
||||
if (typeof native.watchPosition !== 'function') return;
|
||||
safeCall(() =>
|
||||
native.watchPosition(
|
||||
{ enableHighAccuracy: false, timeout: 20000, maximumAge: 60000 },
|
||||
{ enableHighAccuracy: highAccuracy, timeout: 60000, maximumAge: 0 },
|
||||
(position, err) => {
|
||||
if (position) succeed(position);
|
||||
else if (err) console.warn('[geolocation] watch update error:', err);
|
||||
if (position) {
|
||||
succeed(position);
|
||||
} else if (err) {
|
||||
const m = String(
|
||||
(err as { message?: string })?.message ?? err,
|
||||
).toLowerCase();
|
||||
if (m.includes('timeout') || m.includes('timed out')) sawTimeoutish = true;
|
||||
console.warn('[geolocation] watch update error:', err);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.then((id) => {
|
||||
if (settled) {
|
||||
// Already resolved by getCurrentPosition; tidy up this watch.
|
||||
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
||||
} else {
|
||||
watchId = id;
|
||||
watchIds.push(id);
|
||||
}
|
||||
})
|
||||
.catch((watchErr) => {
|
||||
console.warn('[geolocation] watchPosition unavailable:', watchErr);
|
||||
console.warn('[geolocation] watchPosition failed:', watchErr);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 2) getCurrentPosition — instant if the OS has a recent fix.
|
||||
// Run BOTH a high-accuracy (GPS) and low-accuracy (network, if any) watch in
|
||||
// parallel. On GrapheneOS the GPS one is usually what actually delivers, so
|
||||
// high accuracy MUST be engaged — the old low-accuracy-only watch never
|
||||
// fired without a network provider. Whichever returns first wins.
|
||||
startWatch(true);
|
||||
startWatch(false);
|
||||
|
||||
// Also fire a one-shot high-accuracy getCurrentPosition (some plugin
|
||||
// versions deliver here even when the watch is quiet). Cached ok.
|
||||
safeCall(() =>
|
||||
native.getCurrentPosition({
|
||||
enableHighAccuracy: false,
|
||||
timeout: 15000,
|
||||
maximumAge: 120000,
|
||||
enableHighAccuracy: true,
|
||||
timeout: 55000,
|
||||
maximumAge: 60000,
|
||||
}),
|
||||
)
|
||||
.then((pos) => succeed(pos))
|
||||
.catch((gcpErr) => {
|
||||
// Don't fail the whole race — watchPosition may still deliver. Only the
|
||||
// deadline (or watch) decides the outcome. Log for visibility.
|
||||
console.warn('[geolocation] getCurrentPosition (fast) failed:', gcpErr);
|
||||
const m = String(
|
||||
(gcpErr as { message?: string })?.message ?? gcpErr,
|
||||
).toLowerCase();
|
||||
if (m.includes('timeout') || m.includes('timed out')) sawTimeoutish = true;
|
||||
console.warn('[geolocation] getCurrentPosition failed:', gcpErr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -298,21 +332,10 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise<GeoCoord
|
|||
// network/fused fix. We run it in parallel with a cheap getCurrentPosition
|
||||
// and take whichever resolves first. Only if BOTH fail do we surface an
|
||||
// error. This gets an indoor fix without ever asking the user to move.
|
||||
try {
|
||||
return await raceForFirstFix(native, safeCall);
|
||||
} catch (fixErr) {
|
||||
console.warn('[geolocation] fast-fix strategy failed:', fixErr);
|
||||
// Last-ditch: one more high-accuracy attempt with a long window (outdoors
|
||||
// / cold GPS). If this also fails we fall through to the catch below.
|
||||
const pos = await safeCall(() =>
|
||||
native.getCurrentPosition({
|
||||
enableHighAccuracy: true,
|
||||
timeout: 30000,
|
||||
maximumAge: 300000,
|
||||
}),
|
||||
);
|
||||
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
||||
}
|
||||
// raceForFirstFix already runs parallel high+low accuracy watches AND a
|
||||
// one-shot getCurrentPosition for up to 60s, so there's nothing more to try
|
||||
// if it fails — let the error propagate to the classifier below.
|
||||
return await raceForFirstFix(native, safeCall);
|
||||
} catch (err) {
|
||||
if (err instanceof GeolocationError) throw err;
|
||||
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
||||
|
|
|
|||
|
|
@ -629,13 +629,28 @@ export default function MapPage() {
|
|||
setNearMeError(null);
|
||||
setNearMeLoading(true);
|
||||
|
||||
// Getting a GPS lock (especially first-time / on ROMs without Google's
|
||||
// fused provider) can take a while — let the user know it's working.
|
||||
const locatingToast = toast({
|
||||
title: 'Locating you…',
|
||||
description: 'Getting a GPS fix. This can take up to a minute the first time.',
|
||||
duration: 60000,
|
||||
});
|
||||
|
||||
try {
|
||||
const { lat, lng } = await getCurrentPosition();
|
||||
locatingToast.dismiss();
|
||||
setFlyTo({ lat, lng, zoom: 14 });
|
||||
setUserLocation({ lat, lon: lng });
|
||||
setNearMeLoading(false);
|
||||
setNearMeError(null);
|
||||
toast({
|
||||
title: 'Found you',
|
||||
description: 'Centered the map on your location.',
|
||||
duration: 3000,
|
||||
});
|
||||
} catch (err) {
|
||||
locatingToast.dismiss();
|
||||
setNearMeLoading(false);
|
||||
const kind = err instanceof GeolocationError ? err.kind : 'unavailable';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue