FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(core): restore application lifecycle events and window resolution… · NativeScript/NativeScript@7c5469e · GitHub

Commit 7c5469e

Browse files
authored
fix(core): restore application lifecycle events and window resolution under UIScene (#11373)
1 parent 2b2842e commit 7c5469e

8 files changed

Lines changed: 688 additions & 25 deletions
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2+
3+
// `application.android.ts` registers deferred event wiring while it loads, through a hook the
4+
// `NativeScriptGlobals` stub in `vitest.setup.ts` does not carry. Hoisted so it lands before
5+
// the import below.
6+
vi.hoisted(() => {
7+
(globalThis as any).NativeScriptGlobals.addEventWiring = () => {};
8+
});
9+
10+
import { Observable } from '../data/observable';
11+
import { AndroidApplication } from './application.android';
12+
import type { NativeWindow } from '../native-window/native-window-common';
13+
14+
/**
15+
* `vitest.setup.ts` installs a `NativeScriptGlobals` whose event bus methods are no-ops, and
16+
* `ApplicationCommon` binds them per instance. Swapping in a real Observable before each
17+
* Application is constructed is what makes its events observable at all.
18+
*/
19+
function installApplicationEventBus(): Observable {
20+
const events = new Observable();
21+
const bus = (global.NativeScriptGlobals as any).events;
22+
23+
bus.on = events.on.bind(events);
24+
bus.once = events.once.bind(events);
25+
bus.off = events.off.bind(events);
26+
bus.notify = events.notify.bind(events);
27+
bus.hasListeners = events.hasListeners.bind(events);
28+
29+
return events;
30+
}
31+
32+
/** Nothing about the window itself reaches the aggregate — only its identity. */
33+
function createWindow(): NativeWindow {
34+
return {} as unknown as NativeWindow;
35+
}
36+
37+
describe('activity lifecycle aggregate', () => {
38+
let app: AndroidApplication;
39+
let order: string[];
40+
41+
beforeEach(() => {
42+
installApplicationEventBus();
43+
app = new AndroidApplication();
44+
45+
order = [];
46+
for (const eventName of ['resume', 'suspend']) {
47+
app.on(eventName, () => order.push(eventName));
48+
}
49+
});
50+
51+
afterEach(() => {
52+
vi.restoreAllMocks();
53+
installApplicationEventBus();
54+
});
55+
56+
it('raises each app-level event once as a single window moves through the lifecycle', () => {
57+
const window = createWindow();
58+
59+
app._setWindowActive(window, true);
60+
expect(order).toEqual(['resume']);
61+
expect(app.suspended).toBe(false);
62+
63+
app._setWindowActive(window, false);
64+
expect(order).toEqual(['resume', 'suspend']);
65+
expect(app.suspended).toBe(true);
66+
});
67+
68+
it('raises only on the first window in and the last window out', () => {
69+
const first = createWindow();
70+
const second = createWindow();
71+
72+
app._setWindowActive(first, true);
73+
app._setWindowActive(second, true);
74+
expect(order).toEqual(['resume']);
75+
76+
// The window the user switched away from resigns while the other is still on screen.
77+
app._setWindowActive(first, false);
78+
expect(order).toEqual(['resume']);
79+
expect(app.suspended).toBe(false);
80+
81+
app._setWindowActive(second, false);
82+
expect(order).toEqual(['resume', 'suspend']);
83+
expect(app.suspended).toBe(true);
84+
});
85+
86+
it('ignores repeated calls for the same window', () => {
87+
const window = createWindow();
88+
89+
app._setWindowActive(window, true);
90+
app._setWindowActive(window, true);
91+
expect(order).toEqual(['resume']);
92+
93+
app._setWindowActive(window, false);
94+
app._setWindowActive(window, false);
95+
expect(order).toEqual(['resume', 'suspend']);
96+
});
97+
98+
it('ignores a window resigning that never became active', () => {
99+
const window = createWindow();
100+
const other = createWindow();
101+
102+
app._setWindowActive(window, false);
103+
expect(order).toEqual([]);
104+
105+
app._setWindowActive(window, true);
106+
app._setWindowActive(other, false);
107+
expect(order).toEqual(['resume']);
108+
});
109+
110+
it('lets an activity with no registered window speak for the app while no window does', () => {
111+
app._setWindowActive(undefined, true);
112+
expect(order).toEqual(['resume']);
113+
114+
app._setWindowActive(undefined, false);
115+
expect(order).toEqual(['resume', 'suspend']);
116+
});
117+
118+
it('does not let an activity with no registered window suspend an app another window holds active', () => {
119+
const window = createWindow();
120+
121+
app._setWindowActive(window, true);
122+
app._setWindowActive(undefined, false);
123+
124+
expect(order).toEqual(['resume']);
125+
expect(app.suspended).toBe(false);
126+
});
127+
128+
it('carries the activity on the app-level event', () => {
129+
const activity = {} as androidx.appcompat.app.AppCompatActivity;
130+
const received: any[] = [];
131+
app.on('resume', (args) => received.push(args));
132+
133+
app._setWindowActive(createWindow(), true, activity);
134+
135+
expect(received).toHaveLength(1);
136+
expect(received[0].activity).toBe(activity);
137+
expect(received[0].android).toBe(activity);
138+
});
139+
});

‎packages/core/application/application.android.ts‎

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ function initNativeScriptLifecycleCallbacks() {
148148

149149
const nativeWindow = Application.android._getWindowForActivity(activity);
150150
if (nativeWindow) {
151+
// Android always pauses before destroying, so the window has normally left the
152+
// active aggregate already; this only keeps a dropped pause from stranding it
153+
// there and wedging the app as never-suspended.
154+
Application.android._setWindowActive(nativeWindow, false, activity);
155+
151156
// A destroyed activity only ends the window session when it is finishing —
152157
// otherwise Android is recreating it and the same window is reused.
153158
const isClosing = activity.isFinishing();
@@ -195,15 +200,14 @@ function initNativeScriptLifecycleCallbacks() {
195200
@profile
196201
public onActivityPaused(activity: androidx.appcompat.app.AppCompatActivity): void {
197202
// console.log('NativeScriptLifecycleCallbacks onActivityPaused');
203+
const nativeWindow = Application.android._getWindowForActivity(activity);
204+
205+
// `onActivityCreated` registers a window for every activity in the process, so the
206+
// window's role cannot tell a NativeScript activity from a third-party one.
198207
if ('isNativeScriptActivity' in activity) {
199-
Application.setSuspended(true, {
200-
// todo: deprecate event.android in favor of event.activity
201-
android: activity,
202-
activity,
203-
});
208+
Application.android._setWindowActive(nativeWindow, false, activity);
204209
}
205210

206-
const nativeWindow = Application.android._getWindowForActivity(activity);
207211
if (nativeWindow) {
208212
nativeWindow._notifyEvent(NativeWindowEvents.deactivate);
209213
// Emit activityPaused on NativeWindow
@@ -518,6 +522,11 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp
518522
private lifecycleCallbacks: NativeScriptLifecycleCallbacks;
519523
private componentCallbacks: NativeScriptComponentCallbacks;
520524

525+
// Windows currently active. Membership, not a count, so a repeated or missing lifecycle
526+
// callback cannot drift the aggregate. The registry owns window lifetime, so nothing here
527+
// outlives the window itself.
528+
private _activeWindows = new Set<NativeWindow>();
529+
521530
init(nativeApp: android.app.Application): void {
522531
if (this.nativeApp === nativeApp) {
523532
return;
@@ -709,6 +718,44 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp
709718
return this._windows.find((nw) => nw.android?.activity === activity) as AndroidNativeWindow | undefined;
710719
}
711720

721+
/**
722+
* @internal - Feeds a window's active state into the application-level 'resume'/'suspend'
723+
* events, which describe the app as a whole: they are raised when the first window becomes
724+
* active and when the last active one resigns. Callers decide which activities may speak
725+
* for the app; every window reaching here participates.
726+
*/
727+
_setWindowActive(nativeWindow: NativeWindow | undefined, active: boolean, activity?: androidx.appcompat.app.AppCompatActivity): void {
728+
if (this._trackWindowActive(nativeWindow, active)) {
729+
this.setSuspended(!active, {
730+
// todo: deprecate event.android in favor of event.activity
731+
android: activity,
732+
activity,
733+
});
734+
}
735+
}
736+
737+
/**
738+
* @returns whether the set flipped between empty and non-empty, which is the only point
739+
* at which app-level state changes.
740+
*/
741+
private _trackWindowActive(nativeWindow: NativeWindow | undefined, active: boolean): boolean {
742+
const wasPopulated = this._activeWindows.size > 0;
743+
744+
// An activity with no registered window cannot join the aggregate, so it only speaks
745+
// for the app while no window holds the state.
746+
if (!nativeWindow) {
747+
return !wasPopulated;
748+
}
749+
750+
if (active) {
751+
this._activeWindows.add(nativeWindow);
752+
} else {
753+
this._activeWindows.delete(nativeWindow);
754+
}
755+
756+
return wasPopulated !== this._activeWindows.size > 0;
757+
}
758+
712759
// --- Multi-window support ---
713760

714761
/**

‎packages/core/application/application.d.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ export class AndroidApplication extends ApplicationCommon {
155155
*/
156156
_getWindowForActivity(activity: androidx.appcompat.app.AppCompatActivity): NativeWindow | undefined;
157157

158+
/**
159+
* @internal - Feeds a window's active state into the application-level 'resume'/'suspend'
160+
* events, raised when the first window becomes active and when the last active one resigns.
161+
*/
162+
_setWindowActive(nativeWindow: NativeWindow | undefined, active: boolean, activity?: androidx.appcompat.app.AppCompatActivity): void;
163+
158164
/**
159165
* Opens a new window by launching the start activity into its own task.
160166
*

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL