Use Capacitor pause/resume events on Android to gate appActive at the same lifecycle edge as MainActivity.isInForeground.
This commit is contained in:
parent
aaebdffc4d
commit
a35dfb1a5b
2 changed files with 65 additions and 35 deletions
|
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { App } from '@capacitor/app';
|
import { App } from '@capacitor/app';
|
||||||
import { incomingCallsAtom } from '../state/incomingCalls';
|
import { incomingCallsAtom } from '../state/incomingCalls';
|
||||||
import { isNativePlatform } from '../utils/capacitor';
|
import { isAndroidPlatform, isNativePlatform } from '../utils/capacitor';
|
||||||
|
|
||||||
const SUMMARY_NOTIFICATION_ID = -2147483648;
|
const SUMMARY_NOTIFICATION_ID = -2147483648;
|
||||||
|
|
||||||
|
|
@ -60,29 +60,45 @@ export const useDismissNativeCallNotifications = (): void => {
|
||||||
() => typeof document === 'undefined' || document.visibilityState === 'visible'
|
() => typeof document === 'undefined' || document.visibilityState === 'visible'
|
||||||
);
|
);
|
||||||
|
|
||||||
// App state tracking. Race-guard: a real appStateChange firing before
|
// App state tracking.
|
||||||
// getState() resolves must win, otherwise the stale snapshot re-enables the
|
// Android: `pause`/`resume` fire inside BridgeActivity.onPause/onResume via
|
||||||
// foreground branch while we are actually backgrounded (and we'd dismiss
|
// AppPlugin.handleOnPause/Resume — the same Activity callbacks that flip
|
||||||
// the native ring we just raised).
|
// MainActivity.isInForeground — and reach JS within ms via the WebView
|
||||||
|
// bridge. `appStateChange` on Android fires only in BridgeActivity.onStop,
|
||||||
|
// tens of ms to ~1s after onPause; using it as the gate would leak incoming
|
||||||
|
// rings into a "just backgrounded" window where Java sees background (FCM
|
||||||
|
// shows native CallStyle) but JS still sees active (and the dismiss branch
|
||||||
|
// below would clobber the freshly-raised native ring).
|
||||||
|
// iOS/web keeps `appStateChange` because there it maps to
|
||||||
|
// willResignActive/didBecomeActive (iOS) or document.visibilitychange (web)
|
||||||
|
// — the correct edge. The Capacitor `pause`/`resume` events on iOS bind to
|
||||||
|
// didEnterBackground/willEnterForeground, which is a different semantics.
|
||||||
|
// Race-guard: a real lifecycle event arriving before getState() resolves
|
||||||
|
// must win over the stale snapshot.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let sawAppStateEvent = false;
|
let sawLifecycleEvent = false;
|
||||||
let remove: (() => void) | undefined;
|
const handles: Array<{ remove: () => void }> = [];
|
||||||
|
const track = (h: { remove: () => void }) => {
|
||||||
|
if (cancelled) h.remove();
|
||||||
|
else handles.push(h);
|
||||||
|
};
|
||||||
|
|
||||||
App.addListener('appStateChange', ({ isActive }) => {
|
const apply = (isActive: boolean) => {
|
||||||
sawAppStateEvent = true;
|
sawLifecycleEvent = true;
|
||||||
if (!cancelled) setAppActive(isActive);
|
if (!cancelled) setAppActive(isActive);
|
||||||
}).then((handle) => {
|
};
|
||||||
if (cancelled) handle.remove();
|
|
||||||
else
|
if (isAndroidPlatform()) {
|
||||||
remove = () => {
|
App.addListener('pause', () => apply(false)).then(track);
|
||||||
handle.remove();
|
App.addListener('resume', () => apply(true)).then(track);
|
||||||
};
|
} else {
|
||||||
});
|
App.addListener('appStateChange', ({ isActive }) => apply(isActive)).then(track);
|
||||||
|
}
|
||||||
|
|
||||||
App.getState()
|
App.getState()
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
if (!cancelled && !sawAppStateEvent) setAppActive(state.isActive);
|
if (!cancelled && !sawLifecycleEvent) setAppActive(state.isActive);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
/* web fallback handled by initial document.visibilityState */
|
/* web fallback handled by initial document.visibilityState */
|
||||||
|
|
@ -90,7 +106,7 @@ export const useDismissNativeCallNotifications = (): void => {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
remove?.();
|
handles.forEach((h) => h.remove());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { App } from '@capacitor/app';
|
||||||
import { incomingCallsAtom } from '../state/incomingCalls';
|
import { incomingCallsAtom } from '../state/incomingCalls';
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
import { IncomingCallStrip } from '../features/call-status';
|
import { IncomingCallStrip } from '../features/call-status';
|
||||||
|
import { isAndroidPlatform } from '../utils/capacitor';
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
// eslint-disable-next-line import/no-relative-packages
|
||||||
import RingSoundOgg from '../../../public/sound/ring.ogg';
|
import RingSoundOgg from '../../../public/sound/ring.ogg';
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
// eslint-disable-next-line import/no-relative-packages
|
||||||
|
|
@ -44,27 +45,40 @@ export function IncomingCallStripRenderer() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
// Listener registration and getState() race: if a real appStateChange
|
// Android: pause/resume fire on BridgeActivity.onPause/onResume via
|
||||||
// fires before getState() resolves, the stale getState() snapshot would
|
// AppPlugin.handleOnPause/Resume — same Activity callbacks that flip
|
||||||
// overwrite the fresh event and re-open §5.39 (e.g. background → listener
|
// MainActivity.isInForeground — reaching JS within ms via the WebView
|
||||||
// sets false → late getState() resolves true → audio re-enabled).
|
// bridge. appStateChange on Android only fires in onStop (tens of ms to
|
||||||
let sawAppStateEvent = false;
|
// ~1s after onPause), which would let JS audio race the native ringtone
|
||||||
let remove: (() => void) | undefined;
|
// in the "just backgrounded" window and double-ring.
|
||||||
|
// iOS/web keeps appStateChange because there it maps to
|
||||||
|
// willResignActive/didBecomeActive (iOS) or document.visibilitychange
|
||||||
|
// (web). The Capacitor pause/resume events on iOS bind to
|
||||||
|
// didEnterBackground/willEnterForeground — a different edge.
|
||||||
|
// Race-guard: a real lifecycle event arriving before getState() resolves
|
||||||
|
// must win over the stale snapshot.
|
||||||
|
let sawLifecycleEvent = false;
|
||||||
|
const handles: Array<{ remove: () => void }> = [];
|
||||||
|
const track = (h: { remove: () => void }) => {
|
||||||
|
if (cancelled) h.remove();
|
||||||
|
else handles.push(h);
|
||||||
|
};
|
||||||
|
|
||||||
App.addListener('appStateChange', ({ isActive }) => {
|
const apply = (isActive: boolean) => {
|
||||||
sawAppStateEvent = true;
|
sawLifecycleEvent = true;
|
||||||
if (!cancelled) setAppActive(isActive);
|
if (!cancelled) setAppActive(isActive);
|
||||||
}).then((handle) => {
|
};
|
||||||
if (cancelled) handle.remove();
|
|
||||||
else
|
if (isAndroidPlatform()) {
|
||||||
remove = () => {
|
App.addListener('pause', () => apply(false)).then(track);
|
||||||
handle.remove();
|
App.addListener('resume', () => apply(true)).then(track);
|
||||||
};
|
} else {
|
||||||
});
|
App.addListener('appStateChange', ({ isActive }) => apply(isActive)).then(track);
|
||||||
|
}
|
||||||
|
|
||||||
App.getState()
|
App.getState()
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
if (!cancelled && !sawAppStateEvent) setAppActive(state.isActive);
|
if (!cancelled && !sawLifecycleEvent) setAppActive(state.isActive);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
/* web fallback handled by initial document.visibilityState */
|
/* web fallback handled by initial document.visibilityState */
|
||||||
|
|
@ -72,7 +86,7 @@ export function IncomingCallStripRenderer() {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
remove?.();
|
handles.forEach((h) => h.remove());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue