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> {
|
): Promise<GeoCoords> {
|
||||||
return new Promise<GeoCoords>((resolve, reject) => {
|
return new Promise<GeoCoords>((resolve, reject) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
let watchId: string | null = null;
|
const watchIds: string[] = [];
|
||||||
let deadline: ReturnType<typeof setTimeout> | null = null;
|
let deadline: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let sawTimeoutish = false; // remember if the failure looked like a timeout
|
||||||
|
|
||||||
const cleanup = () => {
|
const clearAllWatches = () => {
|
||||||
if (deadline) clearTimeout(deadline);
|
while (watchIds.length) {
|
||||||
if (watchId !== null) {
|
const id = watchIds.pop()!;
|
||||||
const id = watchId;
|
|
||||||
watchId = null;
|
|
||||||
// clearWatch is fire-and-forget; guard so it can't throw into us.
|
|
||||||
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (deadline) clearTimeout(deadline);
|
||||||
|
clearAllWatches();
|
||||||
|
};
|
||||||
|
|
||||||
const succeed = (pos: CapPosition) => {
|
const succeed = (pos: CapPosition) => {
|
||||||
if (settled) return;
|
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;
|
settled = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude });
|
resolve({ lat: latitude, lng: longitude });
|
||||||
};
|
};
|
||||||
|
|
||||||
const fail = (err: unknown) => {
|
const fail = (err: unknown) => {
|
||||||
|
|
@ -211,46 +224,67 @@ function raceForFirstFix(
|
||||||
reject(err instanceof Error ? err : new Error(String(err)));
|
reject(err instanceof Error ? err : new Error(String(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Overall deadline — if nothing arrives, let the caller try high-accuracy.
|
// GrapheneOS (no Play Services) has no fast fused provider — a cold GPS lock
|
||||||
deadline = setTimeout(() => fail(new Error('fast-fix timeout')), 25000);
|
// 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).
|
// Register a watch with the given accuracy; first valid reading wins.
|
||||||
if (typeof native.watchPosition === 'function') {
|
const startWatch = (highAccuracy: boolean) => {
|
||||||
|
if (typeof native.watchPosition !== 'function') return;
|
||||||
safeCall(() =>
|
safeCall(() =>
|
||||||
native.watchPosition(
|
native.watchPosition(
|
||||||
{ enableHighAccuracy: false, timeout: 20000, maximumAge: 60000 },
|
{ enableHighAccuracy: highAccuracy, timeout: 60000, maximumAge: 0 },
|
||||||
(position, err) => {
|
(position, err) => {
|
||||||
if (position) succeed(position);
|
if (position) {
|
||||||
else if (err) console.warn('[geolocation] watch update error:', err);
|
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) => {
|
.then((id) => {
|
||||||
if (settled) {
|
if (settled) {
|
||||||
// Already resolved by getCurrentPosition; tidy up this watch.
|
|
||||||
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
safeCall(() => native.clearWatch({ id })).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
watchId = id;
|
watchIds.push(id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((watchErr) => {
|
.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(() =>
|
safeCall(() =>
|
||||||
native.getCurrentPosition({
|
native.getCurrentPosition({
|
||||||
enableHighAccuracy: false,
|
enableHighAccuracy: true,
|
||||||
timeout: 15000,
|
timeout: 55000,
|
||||||
maximumAge: 120000,
|
maximumAge: 60000,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((pos) => succeed(pos))
|
.then((pos) => succeed(pos))
|
||||||
.catch((gcpErr) => {
|
.catch((gcpErr) => {
|
||||||
// Don't fail the whole race — watchPosition may still deliver. Only the
|
const m = String(
|
||||||
// deadline (or watch) decides the outcome. Log for visibility.
|
(gcpErr as { message?: string })?.message ?? gcpErr,
|
||||||
console.warn('[geolocation] getCurrentPosition (fast) failed:', 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
|
// 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
|
// 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.
|
// error. This gets an indoor fix without ever asking the user to move.
|
||||||
try {
|
// raceForFirstFix already runs parallel high+low accuracy watches AND a
|
||||||
return await raceForFirstFix(native, safeCall);
|
// one-shot getCurrentPosition for up to 60s, so there's nothing more to try
|
||||||
} catch (fixErr) {
|
// if it fails — let the error propagate to the classifier below.
|
||||||
console.warn('[geolocation] fast-fix strategy failed:', fixErr);
|
return await raceForFirstFix(native, safeCall);
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof GeolocationError) throw err;
|
if (err instanceof GeolocationError) throw err;
|
||||||
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -629,13 +629,28 @@ export default function MapPage() {
|
||||||
setNearMeError(null);
|
setNearMeError(null);
|
||||||
setNearMeLoading(true);
|
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 {
|
try {
|
||||||
const { lat, lng } = await getCurrentPosition();
|
const { lat, lng } = await getCurrentPosition();
|
||||||
|
locatingToast.dismiss();
|
||||||
setFlyTo({ lat, lng, zoom: 14 });
|
setFlyTo({ lat, lng, zoom: 14 });
|
||||||
setUserLocation({ lat, lon: lng });
|
setUserLocation({ lat, lon: lng });
|
||||||
setNearMeLoading(false);
|
setNearMeLoading(false);
|
||||||
setNearMeError(null);
|
setNearMeError(null);
|
||||||
|
toast({
|
||||||
|
title: 'Found you',
|
||||||
|
description: 'Centered the map on your location.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
locatingToast.dismiss();
|
||||||
setNearMeLoading(false);
|
setNearMeLoading(false);
|
||||||
const kind = err instanceof GeolocationError ? err.kind : 'unavailable';
|
const kind = err instanceof GeolocationError ? err.kind : 'unavailable';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue