cbe3e0eeaf
- Skip MapLibre on Android <29 (Karoo): SELinux denies kgsl-3d0 access from untrusted_app context, crashing the GPU driver on any OpenGL surface. Replace with SvgRouteView — equirectangular SVG route trace using react-native-svg, no native GL surface needed. - Add +/- zoom buttons to full-screen MapLibre map on modern devices via Camera ref and onRegionDidChange. - Skip PyodideWebView on Android <29: same GPU driver conflict; set _engineUnavailable at module init via API level gate (< 29). - Add engine_unavailable fast path in PyodideWebView: post message immediately if WebAssembly.Global is absent (Chrome <69) instead of attempting 30 MB Pyodide download. - Add server-side extraction fallback (extractServer.ts): when engine unavailable, POST raw file as base64 to /api/upload/raw; server runs full Python pipeline and returns extracted data. - Add /api/upload/raw endpoint in server.py. - Add pre-flight auth check (checkServerAuth) before batch import so an expired token errors immediately rather than after N files. - Fix uploadLocalActivities in sync.ts: was reading original_path as JSON (binary FIT file, always threw), silently skipping every upload. Now reads detail_json from DB directly. - Redesign Feed header: replace single Sync button with Upload / Download / Refresh. Pull-to-refresh and Refresh button are local-only. Auto-refresh on tab focus via useFocusEffect. - Replace ActivityIndicator with plain Text everywhere (native animation also crashes Karoo GPU driver). - Raise macOS open-file limit in dev_test.py to prevent EMFILE errors from Astro file watcher. - Document all Karoo hardware constraints in docs/mobile-app.md.
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { createRef } from 'react';
|
|
import { Platform } from 'react-native';
|
|
import type WebView from 'react-native-webview';
|
|
import type { WebViewMessageEvent } from 'react-native-webview';
|
|
|
|
export type ExtractionResult = {
|
|
id: string;
|
|
detail: object;
|
|
timeseries: object | null;
|
|
geojson: object | null;
|
|
sourceHash: string;
|
|
};
|
|
|
|
type Pending = {
|
|
resolve: (r: ExtractionResult) => void;
|
|
reject: (e: Error) => void;
|
|
onStatus: (msg: string) => void;
|
|
};
|
|
|
|
export const pyodideRef = createRef<WebView>();
|
|
|
|
const pending = new Map<string, Pending>();
|
|
let reqCounter = 0;
|
|
let isExtracting = false;
|
|
|
|
// Engine readiness — tracked so callers can wait before batching files.
|
|
let _engineReady = false;
|
|
let _engineError: string | null = null;
|
|
// Android <29 (API 27 = Android 8.1, e.g. Karoo) ships with a system WebView
|
|
// (Chrome <69) that lacks WebAssembly.Global, so Pyodide cannot run. Mounting
|
|
// a WebView on those devices also causes GPU driver crashes (SurfaceView
|
|
// conflicts). Skip the engine entirely and route to server extraction instead.
|
|
let _engineUnavailable = Platform.OS === 'android' && (Platform.Version as number) < 29;
|
|
const _engineResolvers: Array<() => void> = [];
|
|
const _engineRejecters: Array<(e: Error) => void> = [];
|
|
|
|
// Init-phase progress listeners (messages sent before any extraction starts).
|
|
const _progressListeners = new Set<(msg: string) => void>();
|
|
export function onEngineProgress(cb: (msg: string) => void): () => void {
|
|
_progressListeners.add(cb);
|
|
return () => _progressListeners.delete(cb);
|
|
}
|
|
|
|
export function isEngineAvailable(): boolean | null {
|
|
// null = not yet determined; true = ready; false = unavailable
|
|
if (_engineReady) return true;
|
|
if (_engineUnavailable || _engineError) return false;
|
|
return null;
|
|
}
|
|
|
|
export function waitForEngine(timeoutMs = 300_000): Promise<void> {
|
|
if (_engineReady) return Promise.resolve();
|
|
if (_engineUnavailable) return Promise.reject(new Error('engine_unavailable'));
|
|
if (_engineError) return Promise.reject(new Error(_engineError));
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error('Extraction engine timed out — check network and Bincio instance URL'));
|
|
}, timeoutMs);
|
|
_engineResolvers.push(() => { clearTimeout(timer); resolve(); });
|
|
_engineRejecters.push((e) => { clearTimeout(timer); reject(e); });
|
|
});
|
|
}
|
|
|
|
export function handleWebViewMessage(e: WebViewMessageEvent): void {
|
|
let msg: Record<string, unknown>;
|
|
try { msg = JSON.parse(e.nativeEvent.data); } catch { return; }
|
|
|
|
const reqId = msg.reqId as string | undefined;
|
|
const p = reqId ? pending.get(reqId) : undefined;
|
|
|
|
switch (msg.type) {
|
|
case 'pyodide_ready':
|
|
_engineReady = true;
|
|
_engineResolvers.splice(0).forEach(fn => fn());
|
|
break;
|
|
case 'engine_unavailable':
|
|
_engineUnavailable = true;
|
|
_engineRejecters.splice(0).forEach(fn => fn(new Error('engine_unavailable')));
|
|
break;
|
|
case 'init_error':
|
|
_engineError = msg.message as string;
|
|
_engineRejecters.splice(0).forEach(fn => fn(new Error(_engineError!)));
|
|
break;
|
|
case 'result':
|
|
if (p) {
|
|
pending.delete(reqId!);
|
|
p.resolve({
|
|
id: msg.id as string,
|
|
detail: msg.detail as object,
|
|
timeseries: (msg.timeseries as object | null) ?? null,
|
|
geojson: (msg.geojson as object | null) ?? null,
|
|
sourceHash: msg.sourceHash as string,
|
|
});
|
|
}
|
|
break;
|
|
case 'error':
|
|
if (p) {
|
|
pending.delete(reqId!);
|
|
p.reject(new Error(msg.message as string));
|
|
}
|
|
break;
|
|
case 'progress':
|
|
if (p) {
|
|
p.onStatus(msg.msg as string);
|
|
} else {
|
|
_progressListeners.forEach(fn => fn(msg.msg as string));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// wheelBase64 is the bincio .whl file pre-fetched by the React Native side
|
|
// (native networking supports HTTP on local network; WKWebView does not).
|
|
export function extractFile(
|
|
filename: string,
|
|
base64: string,
|
|
wheelBase64: string,
|
|
wheelFilename: string,
|
|
onStatus: (msg: string) => void = () => {},
|
|
): Promise<ExtractionResult> {
|
|
if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress'));
|
|
|
|
const webview = pyodideRef.current;
|
|
if (!webview) return Promise.reject(new Error('Extraction engine not ready — restart the app'));
|
|
|
|
isExtracting = true;
|
|
const reqId = String(++reqCounter);
|
|
const args = JSON.stringify({ reqId, filename, base64, wheelBase64, wheelFilename });
|
|
|
|
return new Promise<ExtractionResult>((resolve, reject) => {
|
|
pending.set(reqId, {
|
|
resolve: (r) => { isExtracting = false; resolve(r); },
|
|
reject: (e) => { isExtracting = false; reject(e); },
|
|
onStatus,
|
|
});
|
|
webview.injectJavaScript(`window._bincioExtract(${args}); true;`);
|
|
});
|
|
}
|