The reported failures are one missing acknowledgement and one missing service start. - Alarms only fired with the app in focus: the armed WebSocket lives in the native foreground service, and nothing started it — the only entries were a button on one alert screen and a toggle in Settings. The provider now arms the active subscription itself (asks for POST_NOTIFICATIONS, uses the device token the service can hold, throttles a refused start to once per five minutes), and Home says whether the background listener is really running instead of presenting the on-screen socket as coverage. - Dismissed alarms came back: the takeover never sent the ack. acknowledgeAlarm existed and was never called, so the service still believed this device owed the alarm and replayed it on every connect/poll. The acknowledge path now persists the dismissal (durably, retried) and records it locally, so a status report of the trigger it already dismissed cannot raise the takeover again. The record tolerates a dismissal with no event id (the native launch carries only the alert) and is pinned to the concrete event as soon as one is known. - No way back from the takeover: acknowledge now shows a Back to alerts action, hardware back works once the device is silent, and the screen leaves by itself when the alert ends for this device. - A destroyed alert answers 401 to its device token (the subscription went with it). That is terminal: acknowledgements and the status poll stop retrying the dead credential and mark the subscription ended instead of looping. - Settings' start-listening button built the socket URL without the device token, so it could never arm a subscriber's service. tsc clean, 144 tests (15 new: dismissal memory, background arming, dead credential, unnamed ack queue).
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
/**
|
|
* A subscription must ring while the app is not focused.
|
|
*
|
|
* On Android the JS runtime (and therefore the JS socket) stops when the app is
|
|
* backgrounded; the only supported delivery path is the native foreground
|
|
* service, which holds its own WebSocket. Nothing in the app used to start it —
|
|
* the toggle lived on one screen — so a plain subscriber only ever rang while
|
|
* the app was open. These assertions pin the automatic arming and the facts it
|
|
* depends on (a credential the service can use, and its notification).
|
|
*/
|
|
export {};
|
|
|
|
declare const __dirname: string;
|
|
const fs = require('fs') as { readFileSync(p: string, enc: string): string };
|
|
const path = require('path') as { join(...parts: string[]): string };
|
|
|
|
const context = fs.readFileSync(
|
|
path.join(__dirname, '..', 'src', 'state', 'AlarmContext.tsx'),
|
|
'utf8',
|
|
);
|
|
const moduleKt = fs.readFileSync(
|
|
path.join(
|
|
__dirname,
|
|
'..',
|
|
'android',
|
|
'app',
|
|
'src',
|
|
'main',
|
|
'java',
|
|
'com',
|
|
'dcntrlized',
|
|
'alarm',
|
|
'AlarmModule.kt',
|
|
),
|
|
'utf8',
|
|
);
|
|
const home = fs.readFileSync(
|
|
path.join(__dirname, '..', 'src', 'screens', 'HomeScreen.tsx'),
|
|
'utf8',
|
|
);
|
|
|
|
describe('background arming', () => {
|
|
it('starts the foreground service from the provider, not from one screen', () => {
|
|
expect(context).toContain('startForegroundService(');
|
|
expect(context).toContain('alarmSocketUrl(');
|
|
});
|
|
|
|
it('arms the alert the provider is actively listening to', () => {
|
|
const at = context.indexOf('const armedAttemptRef');
|
|
expect(at).toBeGreaterThan(-1);
|
|
const body = context.slice(at, at + 2200);
|
|
expect(body).toContain('activeAlert.alertId');
|
|
expect(body).toContain('activeAlert.subscribeToken');
|
|
// The service cannot refresh a session, so only a device token works here.
|
|
expect(body).toContain('if (!activeAlert.deviceToken) return;');
|
|
expect(body).toContain('activeAlert.deviceToken');
|
|
});
|
|
|
|
it('asks for the notification a foreground service cannot run without', () => {
|
|
expect(context).toContain('requestPermission(');
|
|
expect(context).toContain("'notifications'");
|
|
// The native side refuses to start a service whose notification cannot show.
|
|
expect(moduleKt).toContain('notifications-not-granted');
|
|
});
|
|
|
|
it('does not retry a refused start in a tight loop', () => {
|
|
expect(context).toMatch(/300_000/);
|
|
});
|
|
|
|
it('reports whether the background listener is running, and does not pretend', () => {
|
|
expect(home).toContain('backgroundListening');
|
|
expect(home).toContain('triggers arrive even when this app is closed');
|
|
});
|
|
});
|