Files
alarm-client/__tests__/takeoverLaunch.test.ts
T
Admin 06afd92647 Reach the screen in the background, and leave it again
Measured on an Android 15 emulator with the app backgrounded and the display
asleep: the siren played, the platform refused the takeover
("Background activity launch blocked! … BAL_BLOCK"), and the alarm was
destroyed mid-ring by its own receipt ack.

Takeover:
- the foreground service posts an alarm_firing notification whose
  full-screen intent raises AlarmActivity — the only background launch
  Android 14+ permits. Verified with the display asleep: wakefulness
  Asleep → Awake, topResumedActivity=.AlarmActivity, the FSI on the record.
- the cancel lives on the four real silences (disarm, stop, JS ack, activity
  destroy), not in AlarmPlayer.stop: play() calls stop() first, which
  retracted the notification ~120 ms after posting it.
- "Back to alerts" now finishes the alarm window (AlarmModule
  finishTakeover). A route pop is not an activity: the window stayed on top
  for good with show-when-locked, keep-screen-on and no Recents entry — the
  reported "no way back to the main menu".

The native-delivery contract test said "no notification ever announces an
alarm"; that was falsified on device, so it now pins one FSI notification and
a silent firing channel instead.

Verified end to end on the emulator (two runs, one with the screen off):
join without an account, armed service, background trigger → wake + takeover
+ siren, acknowledge → silence, notification retracted, alert delivered,
takeover closed by itself, Home reachable, reopening silent.
2026-10-19 20:40:38 +01:00

89 lines
3.9 KiB
TypeScript

export {};
/**
* The takeover must reach the screen from the background.
*
* Measured on an Android 15 emulator: the alarm rang with the app backgrounded,
* but `AlarmActivity` was refused by the platform —
* `Background activity launch blocked! … BAL_BLOCK` — because an activity launched
* from the alarm's own PendingIntent is subject to the Android 14+ background
* launch rules. The siren was audible and nothing was on screen.
*
* The platform's sanctioned path is a notification **full-screen intent**, which
* SystemUI launches. These assertions pin that structure, because it cannot be
* exercised in a Jest run (no Android runtime): the firing notification must
* carry the FSI, the alarm category and the takeover's own intent, and silencing
* the alarm must retract it.
*/
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 NATIVE = path.join(
__dirname,
'..',
'android',
'app',
'src',
'main',
'java',
'com',
'dcntrlized',
'alarm',
);
const player = fs.readFileSync(path.join(NATIVE, 'AlarmPlayer.kt'), 'utf8');
const service = fs.readFileSync(path.join(NATIVE, 'AlarmForegroundService.kt'), 'utf8');
describe('background takeover launch', () => {
it('posts a full-screen-intent notification for a trigger', () => {
expect(player).toContain('fun showFiring(');
expect(player).toContain('setFullScreenIntent(open, true)');
expect(player).toContain('Notification.CATEGORY_ALARM');
expect(player).toContain('AlarmActivity.intent(context, alertId, message)');
});
it('raises it from the service that received the frame', () => {
const at = service.indexOf('"trigger" ->');
expect(at).toBeGreaterThan(-1);
const branch = service.slice(at, at + 700);
expect(branch).toContain('AlarmNotifier.showFiring(');
expect(branch.indexOf('showFiring')).toBeLessThan(branch.indexOf('AlarmPlayer.play'));
});
it('retracts it when the alarm is silenced — but not from AlarmPlayer.stop', () => {
// play() calls stop() first, so cancelling there retracted the full-screen
// intent on every single trigger (measured: NotificationService "Cannot find
// enqueued record for key 4713" 120 ms after the trigger).
const stop = player.slice(player.indexOf('fun stop(context: Context)'));
expect(stop.slice(0, 1200)).not.toContain('cancelFiring');
expect(service).toContain('AlarmNotifier.cancelFiring(this)');
// A disarm and an explicit stop both retract it.
expect(service.split('AlarmNotifier.cancelFiring(this)').length - 1).toBeGreaterThanOrEqual(2);
});
it('closes its own window when the user leaves — a route pop is not enough', () => {
// Measured on the emulator: after acknowledging, the app returned to its
// previous screen but the AlarmActivity window stayed on top for good
// (show-when-locked, keep-screen-on, excludeFromRecents) — the reported
// "no way back to the main menu".
const activity = fs.readFileSync(path.join(NATIVE, 'AlarmActivity.kt'), 'utf8');
const bridge = fs.readFileSync(path.join(NATIVE, 'AlarmModule.kt'), 'utf8');
const screen = fs.readFileSync(
path.join(__dirname, '..', 'src', 'screens', 'LiveAlarmScreen.tsx'),
'utf8',
);
expect(activity).toContain('fun currentActivity()');
expect(activity).toContain('current = null');
const finishAt = bridge.indexOf('fun finishTakeover(');
expect(finishAt).toBeGreaterThan(-1);
expect(bridge.slice(finishAt, finishAt + 400)).toContain('.finish()');
const leaveAt = screen.indexOf('const leave = useCallback(');
expect(screen.slice(leaveAt, leaveAt + 600)).toContain('finishTakeover()');
});
it('tells the truth when the FSI grant is missing', () => {
expect(player).toContain('canUseFullScreenIntent()');
expect(player).toContain('heads-up only');
});
});