Skip to content

Commit fe4c9c0

Browse files
authored
feat(android): support independent broadcast listeners (#10936)
1 parent bbeca52 commit fe4c9c0

File tree

8 files changed

+137
-135
lines changed

8 files changed

+137
-135
lines changed

packages/core/application/application.android.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ import {
4040
isA11yEnabled,
4141
setA11yEnabled,
4242
} from '../accessibility/accessibility-common';
43-
import { androidGetForegroundActivity, androidGetStartActivity, androidPendingReceiverRegistrations, androidRegisterBroadcastReceiver, androidRegisteredReceivers, androidSetForegroundActivity, androidSetStartActivity, androidUnregisterBroadcastReceiver, applyContentDescription } from './helpers';
43+
import { androidGetForegroundActivity, androidGetStartActivity, androidSetForegroundActivity, androidSetStartActivity, applyContentDescription } from './helpers';
4444
import { getImageFetcher, getNativeApp, getRootView, initImageCache, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setNativeApp, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
4545
import { getNativeScriptGlobals } from '../globals/global-utils';
46+
import type { AndroidApplication as IAndroidApplication } from './application';
47+
import lazy from '../utils/lazy';
4648

4749
declare class NativeScriptLifecycleCallbacks extends android.app.Application.ActivityLifecycleCallbacks {}
4850

@@ -276,7 +278,36 @@ function initNativeScriptComponentCallbacks() {
276278
return NativeScriptComponentCallbacks_;
277279
}
278280

279-
export class AndroidApplication extends ApplicationCommon {
281+
interface RegisteredReceiverInfo {
282+
receiver: android.content.BroadcastReceiver;
283+
intent: string;
284+
callback: (context: android.content.Context, intent: android.content.Intent) => void;
285+
id: number;
286+
flags: number;
287+
}
288+
289+
const BroadcastReceiver = lazy(() => {
290+
@NativeClass
291+
class BroadcastReceiverImpl extends android.content.BroadcastReceiver {
292+
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
293+
294+
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
295+
super();
296+
this._onReceiveCallback = onReceiveCallback;
297+
298+
return global.__native(this);
299+
}
300+
301+
public onReceive(context: android.content.Context, intent: android.content.Intent) {
302+
if (this._onReceiveCallback) {
303+
this._onReceiveCallback(context, intent);
304+
}
305+
}
306+
}
307+
return BroadcastReceiverImpl;
308+
});
309+
310+
export class AndroidApplication extends ApplicationCommon implements IAndroidApplication {
280311
static readonly activityCreatedEvent = 'activityCreated';
281312
static readonly activityDestroyedEvent = 'activityDestroyed';
282313
static readonly activityStartedEvent = 'activityStarted';
@@ -332,10 +363,13 @@ export class AndroidApplication extends ApplicationCommon {
332363

333364
this._registerPendingReceivers();
334365
}
335-
366+
private _registeredReceivers: Record<string, RegisteredReceiverInfo[]> = {};
367+
private _registeredReceiversById: Record<number, RegisteredReceiverInfo> = {};
368+
private _nextReceiverId: number = 1;
369+
private _pendingReceiverRegistrations: Omit<RegisteredReceiverInfo, 'receiver'>[] = [];
336370
private _registerPendingReceivers() {
337-
androidPendingReceiverRegistrations.forEach((func) => func(this.context));
338-
androidPendingReceiverRegistrations.length = 0;
371+
this._pendingReceiverRegistrations.forEach((info) => this._registerReceiver(this.context, info.intent, info.callback, info.flags, info.id));
372+
this._pendingReceiverRegistrations.length = 0;
339373
}
340374

341375
onConfigurationChanged(configuration: android.content.res.Configuration): void {
@@ -414,18 +448,69 @@ export class AndroidApplication extends ApplicationCommon {
414448
// RECEIVER_EXPORTED (2)
415449
// RECEIVER_NOT_EXPORTED (4)
416450
// RECEIVER_VISIBLE_TO_INSTANT_APPS (1)
417-
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {
418-
androidRegisterBroadcastReceiver(intentFilter, onReceiveCallback, flags);
451+
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): () => void {
452+
const receiverId = this._nextReceiverId++;
453+
if (this.context) {
454+
this._registerReceiver(this.context, intentFilter, onReceiveCallback, flags, receiverId);
455+
} else {
456+
this._pendingReceiverRegistrations.push({
457+
intent: intentFilter,
458+
callback: onReceiveCallback,
459+
id: receiverId,
460+
flags,
461+
});
462+
}
463+
let removed = false;
464+
return () => {
465+
if (removed) {
466+
return;
467+
}
468+
removed = true;
469+
if (this._registeredReceiversById[receiverId]) {
470+
const receiverInfo = this._registeredReceiversById[receiverId];
471+
this.context.unregisterReceiver(receiverInfo.receiver);
472+
this._registeredReceivers[receiverInfo.intent] = this._registeredReceivers[receiverInfo.intent]?.filter((ri) => ri.id !== receiverId);
473+
delete this._registeredReceiversById[receiverId];
474+
} else {
475+
this._pendingReceiverRegistrations = this._pendingReceiverRegistrations.filter((ri) => ri.id !== receiverId);
476+
}
477+
};
478+
}
479+
private _registerReceiver(context: android.content.Context, intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags: number, id: number): android.content.BroadcastReceiver {
480+
const receiver: android.content.BroadcastReceiver = new (BroadcastReceiver())(onReceiveCallback);
481+
if (SDK_VERSION >= 26) {
482+
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter), flags);
483+
} else {
484+
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
485+
}
486+
const receiverInfo: RegisteredReceiverInfo = { receiver, intent: intentFilter, callback: onReceiveCallback, id: typeof id === 'number' ? id : this._nextReceiverId++, flags };
487+
this._registeredReceivers[intentFilter] ??= [];
488+
this._registeredReceivers[intentFilter].push(receiverInfo);
489+
this._registeredReceiversById[receiverInfo.id] = receiverInfo;
490+
return receiver;
419491
}
420492

421493
public unregisterBroadcastReceiver(intentFilter: string): void {
422-
androidUnregisterBroadcastReceiver(intentFilter);
494+
const receivers = this._registeredReceivers[intentFilter];
495+
if (receivers) {
496+
receivers.forEach((receiver) => {
497+
this.context.unregisterReceiver(receiver.receiver);
498+
});
499+
this._registeredReceivers[intentFilter] = [];
500+
}
423501
}
424502

425503
public getRegisteredBroadcastReceiver(intentFilter: string): android.content.BroadcastReceiver | undefined {
426-
return androidRegisteredReceivers[intentFilter];
504+
return this._registeredReceivers[intentFilter]?.[0].receiver;
427505
}
428506

507+
public getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[] {
508+
const receiversInfo = this._registeredReceivers[intentFilter];
509+
if (receiversInfo) {
510+
return receiversInfo.map((info) => info.receiver);
511+
}
512+
return [];
513+
}
429514
getRootView(): View {
430515
const activity = this.foregroundActivity || this.startActivity;
431516
if (!activity) {

packages/core/application/application.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ export class AndroidApplication extends ApplicationCommon {
113113
* For more information, please visit 'http://developer.android.com/reference/android/content/Context.html#registerReceiver%28android.content.BroadcastReceiver,%20android.content.IntentFilter%29'
114114
* @param intentFilter A string containing the intent filter.
115115
* @param onReceiveCallback A callback function that will be called each time the receiver receives a broadcast.
116+
* @return A function that can be called to unregister the receiver.
116117
*/
117-
registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void): void;
118+
registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void): () => void;
118119

119120
/**
120121
* Unregister a previously registered BroadcastReceiver.
@@ -126,8 +127,14 @@ export class AndroidApplication extends ApplicationCommon {
126127
/**
127128
* Get a registered BroadcastReceiver, then you can get the result code of BroadcastReceiver in onReceiveCallback method.
128129
* @param intentFilter A string containing the intent filter.
130+
* @deprecated Use `getRegisteredBroadcastReceivers` instead.
129131
*/
130132
getRegisteredBroadcastReceiver(intentFilter: string): android.content.BroadcastReceiver;
133+
/**
134+
* Get all registered BroadcastReceivers for a specific intent filter.
135+
* @param intentFilter a string containing the intent filter
136+
*/
137+
getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[];
131138

132139
on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void;
133140
on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void;

packages/core/application/application.ios.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,28 @@ import {
4646
setA11yEnabled,
4747
enforceArray,
4848
} from '../accessibility/accessibility-common';
49-
import { iosAddNotificationObserver, iosRemoveNotificationObserver } from './helpers';
5049
import { getiOSWindow, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setiOSWindow, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
5150

51+
@NativeClass
52+
class NotificationObserver extends NSObject {
53+
private _onReceiveCallback: (notification: NSNotification) => void;
54+
55+
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
56+
const observer = <NotificationObserver>super.new();
57+
observer._onReceiveCallback = onReceiveCallback;
58+
59+
return observer;
60+
}
61+
62+
public onReceive(notification: NSNotification): void {
63+
this._onReceiveCallback(notification);
64+
}
65+
66+
public static ObjCExposedMethods = {
67+
onReceive: { returns: interop.types.void, params: [NSNotification] },
68+
};
69+
}
70+
5271
@NativeClass
5372
class CADisplayLinkTarget extends NSObject {
5473
private _owner: WeakRef<iOSApplication>;
@@ -243,6 +262,8 @@ export class iOSApplication extends ApplicationCommon {
243262
private _primaryScene: UIWindowScene | null = null;
244263
private _openedScenesById = new Map<string, UIWindowScene>();
245264

265+
private _notificationObservers: NotificationObserver[] = [];
266+
246267
displayedOnce = false;
247268
displayedLinkTarget: CADisplayLinkTarget;
248269
displayedLink: CADisplayLink;
@@ -477,11 +498,19 @@ export class iOSApplication extends ApplicationCommon {
477498
}
478499

479500
addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
480-
return iosAddNotificationObserver(notificationName, onReceiveCallback);
501+
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
502+
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
503+
this._notificationObservers.push(observer);
504+
505+
return observer;
481506
}
482507

483508
removeNotificationObserver(observer: any /* NotificationObserver */, notificationName: string) {
484-
iosRemoveNotificationObserver(observer, notificationName);
509+
const index = this._notificationObservers.indexOf(observer);
510+
if (index >= 0) {
511+
this._notificationObservers.splice(index, 1);
512+
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
513+
}
485514
}
486515

487516
protected getSystemAppearance(): 'light' | 'dark' {

packages/core/application/helpers.android.ts

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -26,68 +26,6 @@ function getApplicationContext(): android.content.Context {
2626
return getNativeApp<android.app.Application>().getApplicationContext();
2727
}
2828

29-
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver } = {};
30-
export const androidPendingReceiverRegistrations = new Array<(context: android.content.Context) => void>();
31-
32-
declare class BroadcastReceiver extends android.content.BroadcastReceiver {
33-
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void);
34-
}
35-
36-
let BroadcastReceiver_: typeof BroadcastReceiver;
37-
function initBroadcastReceiver() {
38-
if (BroadcastReceiver_) {
39-
return BroadcastReceiver_;
40-
}
41-
42-
@NativeClass
43-
class BroadcastReceiverImpl extends android.content.BroadcastReceiver {
44-
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
45-
46-
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
47-
super();
48-
this._onReceiveCallback = onReceiveCallback;
49-
50-
return global.__native(this);
51-
}
52-
53-
public onReceive(context: android.content.Context, intent: android.content.Intent) {
54-
if (this._onReceiveCallback) {
55-
this._onReceiveCallback(context, intent);
56-
}
57-
}
58-
}
59-
60-
BroadcastReceiver_ = BroadcastReceiverImpl;
61-
return BroadcastReceiver_;
62-
}
63-
64-
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {
65-
const registerFunc = (context: android.content.Context) => {
66-
const receiver: android.content.BroadcastReceiver = new (initBroadcastReceiver())(onReceiveCallback);
67-
if (SDK_VERSION >= 26) {
68-
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter), flags);
69-
} else {
70-
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
71-
}
72-
androidRegisteredReceivers[intentFilter] = receiver;
73-
};
74-
75-
if (getApplicationContext()) {
76-
registerFunc(getApplicationContext());
77-
} else {
78-
androidPendingReceiverRegistrations.push(registerFunc);
79-
}
80-
}
81-
82-
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {
83-
const receiver = androidRegisteredReceivers[intentFilter];
84-
if (receiver) {
85-
getApplicationContext().unregisterReceiver(receiver);
86-
androidRegisteredReceivers[intentFilter] = undefined;
87-
delete androidRegisteredReceivers[intentFilter];
88-
}
89-
}
90-
9129
export function updateContentDescription(view: any /* View */, forceUpdate?: boolean): string | null {
9230
if (!view.nativeViewProtected) {
9331
return;
@@ -204,6 +142,3 @@ export function setupAccessibleView(view: any /* any */): void {
204142
}
205143

206144
// stubs
207-
export const iosNotificationObservers: Array<any> = [];
208-
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: any) => void) {}
209-
export function iosRemoveNotificationObserver(observer: any, notificationName: string) {}

packages/core/application/helpers.d.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,8 @@ export function setupAccessibleView(view: View): void;
88
export const updateContentDescription: (view: any /* View */, forceUpdate?: boolean) => string | null;
99
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean);
1010
/* Android app-wide helpers */
11-
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver };
12-
export const androidPendingReceiverRegistrations: Array<(context: android.content.Context) => void>;
13-
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void;
14-
export function androidUnregisterBroadcastReceiver(intentFilter: string): void;
1511
export function androidGetCurrentActivity(): androidx.appcompat.app.AppCompatActivity;
1612
export function androidGetForegroundActivity(): androidx.appcompat.app.AppCompatActivity;
1713
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
1814
export function androidGetStartActivity(): androidx.appcompat.app.AppCompatActivity;
1915
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
20-
21-
/* iOS app-wide helpers */
22-
export const iosNotificationObservers: NotificationObserver[];
23-
class NotificationObserver extends NSObject {}
24-
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void): NotificationObserver;
25-
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string): void;

packages/core/application/helpers.ios.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,12 @@ export const updateContentDescription = (view: any /* View */, forceUpdate?: boo
33
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean) {
44
return null;
55
}
6-
export const androidRegisteredReceivers = undefined;
7-
export const androidPendingReceiverRegistrations = undefined;
8-
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {}
9-
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {}
106
export function androidGetCurrentActivity() {}
117
export function androidGetForegroundActivity() {}
128
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
139
export function androidGetStartActivity() {}
1410
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
1511

16-
@NativeClass
17-
class NotificationObserver extends NSObject {
18-
private _onReceiveCallback: (notification: NSNotification) => void;
19-
20-
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
21-
const observer = <NotificationObserver>super.new();
22-
observer._onReceiveCallback = onReceiveCallback;
23-
24-
return observer;
25-
}
26-
27-
public onReceive(notification: NSNotification): void {
28-
this._onReceiveCallback(notification);
29-
}
30-
31-
public static ObjCExposedMethods = {
32-
onReceive: { returns: interop.types.void, params: [NSNotification] },
33-
};
34-
}
35-
36-
export const iosNotificationObservers: NotificationObserver[] = [];
37-
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
38-
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
39-
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
40-
iosNotificationObservers.push(observer);
41-
42-
return observer;
43-
}
44-
45-
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string) {
46-
// TODO: test if this finds the right observer instance match everytime
47-
// after circular dependencies are resolved
48-
const index = iosNotificationObservers.indexOf(observer);
49-
if (index >= 0) {
50-
iosNotificationObservers.splice(index, 1);
51-
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
52-
}
53-
}
54-
5512
export function setupAccessibleView(view: any /* any */): void {
5613
const uiView = view.nativeViewProtected as UIView;
5714
if (!uiView) {

0 commit comments

Comments
 (0)