feat(android): support independent broadcast listeners (#10936)

This commit is contained in:
Eduardo Speroni
2025-11-04 14:29:09 -03:00
committed by GitHub
parent bbeca526f0
commit fe4c9c0b7d
8 changed files with 137 additions and 135 deletions

View File

@@ -40,9 +40,11 @@ import {
isA11yEnabled,
setA11yEnabled,
} from '../accessibility/accessibility-common';
import { androidGetForegroundActivity, androidGetStartActivity, androidPendingReceiverRegistrations, androidRegisterBroadcastReceiver, androidRegisteredReceivers, androidSetForegroundActivity, androidSetStartActivity, androidUnregisterBroadcastReceiver, applyContentDescription } from './helpers';
import { androidGetForegroundActivity, androidGetStartActivity, androidSetForegroundActivity, androidSetStartActivity, applyContentDescription } from './helpers';
import { getImageFetcher, getNativeApp, getRootView, initImageCache, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setNativeApp, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
import { getNativeScriptGlobals } from '../globals/global-utils';
import type { AndroidApplication as IAndroidApplication } from './application';
import lazy from '../utils/lazy';
declare class NativeScriptLifecycleCallbacks extends android.app.Application.ActivityLifecycleCallbacks {}
@@ -276,7 +278,36 @@ function initNativeScriptComponentCallbacks() {
return NativeScriptComponentCallbacks_;
}
export class AndroidApplication extends ApplicationCommon {
interface RegisteredReceiverInfo {
receiver: android.content.BroadcastReceiver;
intent: string;
callback: (context: android.content.Context, intent: android.content.Intent) => void;
id: number;
flags: number;
}
const BroadcastReceiver = lazy(() => {
@NativeClass
class BroadcastReceiverImpl extends android.content.BroadcastReceiver {
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
super();
this._onReceiveCallback = onReceiveCallback;
return global.__native(this);
}
public onReceive(context: android.content.Context, intent: android.content.Intent) {
if (this._onReceiveCallback) {
this._onReceiveCallback(context, intent);
}
}
}
return BroadcastReceiverImpl;
});
export class AndroidApplication extends ApplicationCommon implements IAndroidApplication {
static readonly activityCreatedEvent = 'activityCreated';
static readonly activityDestroyedEvent = 'activityDestroyed';
static readonly activityStartedEvent = 'activityStarted';
@@ -332,10 +363,13 @@ export class AndroidApplication extends ApplicationCommon {
this._registerPendingReceivers();
}
private _registeredReceivers: Record<string, RegisteredReceiverInfo[]> = {};
private _registeredReceiversById: Record<number, RegisteredReceiverInfo> = {};
private _nextReceiverId: number = 1;
private _pendingReceiverRegistrations: Omit<RegisteredReceiverInfo, 'receiver'>[] = [];
private _registerPendingReceivers() {
androidPendingReceiverRegistrations.forEach((func) => func(this.context));
androidPendingReceiverRegistrations.length = 0;
this._pendingReceiverRegistrations.forEach((info) => this._registerReceiver(this.context, info.intent, info.callback, info.flags, info.id));
this._pendingReceiverRegistrations.length = 0;
}
onConfigurationChanged(configuration: android.content.res.Configuration): void {
@@ -414,18 +448,69 @@ export class AndroidApplication extends ApplicationCommon {
// RECEIVER_EXPORTED (2)
// RECEIVER_NOT_EXPORTED (4)
// RECEIVER_VISIBLE_TO_INSTANT_APPS (1)
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {
androidRegisterBroadcastReceiver(intentFilter, onReceiveCallback, flags);
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): () => void {
const receiverId = this._nextReceiverId++;
if (this.context) {
this._registerReceiver(this.context, intentFilter, onReceiveCallback, flags, receiverId);
} else {
this._pendingReceiverRegistrations.push({
intent: intentFilter,
callback: onReceiveCallback,
id: receiverId,
flags,
});
}
let removed = false;
return () => {
if (removed) {
return;
}
removed = true;
if (this._registeredReceiversById[receiverId]) {
const receiverInfo = this._registeredReceiversById[receiverId];
this.context.unregisterReceiver(receiverInfo.receiver);
this._registeredReceivers[receiverInfo.intent] = this._registeredReceivers[receiverInfo.intent]?.filter((ri) => ri.id !== receiverId);
delete this._registeredReceiversById[receiverId];
} else {
this._pendingReceiverRegistrations = this._pendingReceiverRegistrations.filter((ri) => ri.id !== receiverId);
}
};
}
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 {
const receiver: android.content.BroadcastReceiver = new (BroadcastReceiver())(onReceiveCallback);
if (SDK_VERSION >= 26) {
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter), flags);
} else {
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
}
const receiverInfo: RegisteredReceiverInfo = { receiver, intent: intentFilter, callback: onReceiveCallback, id: typeof id === 'number' ? id : this._nextReceiverId++, flags };
this._registeredReceivers[intentFilter] ??= [];
this._registeredReceivers[intentFilter].push(receiverInfo);
this._registeredReceiversById[receiverInfo.id] = receiverInfo;
return receiver;
}
public unregisterBroadcastReceiver(intentFilter: string): void {
androidUnregisterBroadcastReceiver(intentFilter);
const receivers = this._registeredReceivers[intentFilter];
if (receivers) {
receivers.forEach((receiver) => {
this.context.unregisterReceiver(receiver.receiver);
});
this._registeredReceivers[intentFilter] = [];
}
}
public getRegisteredBroadcastReceiver(intentFilter: string): android.content.BroadcastReceiver | undefined {
return androidRegisteredReceivers[intentFilter];
return this._registeredReceivers[intentFilter]?.[0].receiver;
}
public getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[] {
const receiversInfo = this._registeredReceivers[intentFilter];
if (receiversInfo) {
return receiversInfo.map((info) => info.receiver);
}
return [];
}
getRootView(): View {
const activity = this.foregroundActivity || this.startActivity;
if (!activity) {

View File

@@ -113,8 +113,9 @@ export class AndroidApplication extends ApplicationCommon {
* For more information, please visit 'http://developer.android.com/reference/android/content/Context.html#registerReceiver%28android.content.BroadcastReceiver,%20android.content.IntentFilter%29'
* @param intentFilter A string containing the intent filter.
* @param onReceiveCallback A callback function that will be called each time the receiver receives a broadcast.
* @return A function that can be called to unregister the receiver.
*/
registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void): void;
registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void): () => void;
/**
* Unregister a previously registered BroadcastReceiver.
@@ -126,8 +127,14 @@ export class AndroidApplication extends ApplicationCommon {
/**
* Get a registered BroadcastReceiver, then you can get the result code of BroadcastReceiver in onReceiveCallback method.
* @param intentFilter A string containing the intent filter.
* @deprecated Use `getRegisteredBroadcastReceivers` instead.
*/
getRegisteredBroadcastReceiver(intentFilter: string): android.content.BroadcastReceiver;
/**
* Get all registered BroadcastReceivers for a specific intent filter.
* @param intentFilter a string containing the intent filter
*/
getRegisteredBroadcastReceivers(intentFilter: string): android.content.BroadcastReceiver[];
on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any): void;
on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any): void;

View File

@@ -46,9 +46,28 @@ import {
setA11yEnabled,
enforceArray,
} from '../accessibility/accessibility-common';
import { iosAddNotificationObserver, iosRemoveNotificationObserver } from './helpers';
import { getiOSWindow, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setiOSWindow, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
@NativeClass
class NotificationObserver extends NSObject {
private _onReceiveCallback: (notification: NSNotification) => void;
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
const observer = <NotificationObserver>super.new();
observer._onReceiveCallback = onReceiveCallback;
return observer;
}
public onReceive(notification: NSNotification): void {
this._onReceiveCallback(notification);
}
public static ObjCExposedMethods = {
onReceive: { returns: interop.types.void, params: [NSNotification] },
};
}
@NativeClass
class CADisplayLinkTarget extends NSObject {
private _owner: WeakRef<iOSApplication>;
@@ -243,6 +262,8 @@ export class iOSApplication extends ApplicationCommon {
private _primaryScene: UIWindowScene | null = null;
private _openedScenesById = new Map<string, UIWindowScene>();
private _notificationObservers: NotificationObserver[] = [];
displayedOnce = false;
displayedLinkTarget: CADisplayLinkTarget;
displayedLink: CADisplayLink;
@@ -477,11 +498,19 @@ export class iOSApplication extends ApplicationCommon {
}
addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
return iosAddNotificationObserver(notificationName, onReceiveCallback);
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
this._notificationObservers.push(observer);
return observer;
}
removeNotificationObserver(observer: any /* NotificationObserver */, notificationName: string) {
iosRemoveNotificationObserver(observer, notificationName);
const index = this._notificationObservers.indexOf(observer);
if (index >= 0) {
this._notificationObservers.splice(index, 1);
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
}
}
protected getSystemAppearance(): 'light' | 'dark' {

View File

@@ -26,68 +26,6 @@ function getApplicationContext(): android.content.Context {
return getNativeApp<android.app.Application>().getApplicationContext();
}
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver } = {};
export const androidPendingReceiverRegistrations = new Array<(context: android.content.Context) => void>();
declare class BroadcastReceiver extends android.content.BroadcastReceiver {
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void);
}
let BroadcastReceiver_: typeof BroadcastReceiver;
function initBroadcastReceiver() {
if (BroadcastReceiver_) {
return BroadcastReceiver_;
}
@NativeClass
class BroadcastReceiverImpl extends android.content.BroadcastReceiver {
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
super();
this._onReceiveCallback = onReceiveCallback;
return global.__native(this);
}
public onReceive(context: android.content.Context, intent: android.content.Intent) {
if (this._onReceiveCallback) {
this._onReceiveCallback(context, intent);
}
}
}
BroadcastReceiver_ = BroadcastReceiverImpl;
return BroadcastReceiver_;
}
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {
const registerFunc = (context: android.content.Context) => {
const receiver: android.content.BroadcastReceiver = new (initBroadcastReceiver())(onReceiveCallback);
if (SDK_VERSION >= 26) {
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter), flags);
} else {
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
}
androidRegisteredReceivers[intentFilter] = receiver;
};
if (getApplicationContext()) {
registerFunc(getApplicationContext());
} else {
androidPendingReceiverRegistrations.push(registerFunc);
}
}
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {
const receiver = androidRegisteredReceivers[intentFilter];
if (receiver) {
getApplicationContext().unregisterReceiver(receiver);
androidRegisteredReceivers[intentFilter] = undefined;
delete androidRegisteredReceivers[intentFilter];
}
}
export function updateContentDescription(view: any /* View */, forceUpdate?: boolean): string | null {
if (!view.nativeViewProtected) {
return;
@@ -204,6 +142,3 @@ export function setupAccessibleView(view: any /* any */): void {
}
// stubs
export const iosNotificationObservers: Array<any> = [];
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: any) => void) {}
export function iosRemoveNotificationObserver(observer: any, notificationName: string) {}

View File

@@ -8,18 +8,8 @@ export function setupAccessibleView(view: View): void;
export const updateContentDescription: (view: any /* View */, forceUpdate?: boolean) => string | null;
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean);
/* Android app-wide helpers */
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver };
export const androidPendingReceiverRegistrations: Array<(context: android.content.Context) => void>;
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void;
export function androidUnregisterBroadcastReceiver(intentFilter: string): void;
export function androidGetCurrentActivity(): androidx.appcompat.app.AppCompatActivity;
export function androidGetForegroundActivity(): androidx.appcompat.app.AppCompatActivity;
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
export function androidGetStartActivity(): androidx.appcompat.app.AppCompatActivity;
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
/* iOS app-wide helpers */
export const iosNotificationObservers: NotificationObserver[];
class NotificationObserver extends NSObject {}
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void): NotificationObserver;
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string): void;

View File

@@ -3,55 +3,12 @@ export const updateContentDescription = (view: any /* View */, forceUpdate?: boo
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean) {
return null;
}
export const androidRegisteredReceivers = undefined;
export const androidPendingReceiverRegistrations = undefined;
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {}
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {}
export function androidGetCurrentActivity() {}
export function androidGetForegroundActivity() {}
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
export function androidGetStartActivity() {}
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
@NativeClass
class NotificationObserver extends NSObject {
private _onReceiveCallback: (notification: NSNotification) => void;
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
const observer = <NotificationObserver>super.new();
observer._onReceiveCallback = onReceiveCallback;
return observer;
}
public onReceive(notification: NSNotification): void {
this._onReceiveCallback(notification);
}
public static ObjCExposedMethods = {
onReceive: { returns: interop.types.void, params: [NSNotification] },
};
}
export const iosNotificationObservers: NotificationObserver[] = [];
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
iosNotificationObservers.push(observer);
return observer;
}
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string) {
// TODO: test if this finds the right observer instance match everytime
// after circular dependencies are resolved
const index = iosNotificationObservers.indexOf(observer);
if (index >= 0) {
iosNotificationObservers.splice(index, 1);
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
}
}
export function setupAccessibleView(view: any /* any */): void {
const uiView = view.nativeViewProtected as UIView;
if (!uiView) {

View File

@@ -1,6 +1,6 @@
import { getNativeApp } from '../application/helpers-common';
import { androidRegisterBroadcastReceiver, androidUnregisterBroadcastReceiver } from '../application/helpers';
import { SDK_VERSION } from '../utils/constants';
import { Application } from '../application';
export enum connectionType {
none = 0,
@@ -110,7 +110,7 @@ function startMonitoringLegacy(connectionTypeChangedCallback) {
connectionTypeChangedCallback(newConnectionType);
};
const zoneCallback = zonedCallback(onReceiveCallback);
androidRegisterBroadcastReceiver(android.net.ConnectivityManager.CONNECTIVITY_ACTION, zoneCallback);
Application.android.registerBroadcastReceiver(android.net.ConnectivityManager.CONNECTIVITY_ACTION, zoneCallback);
}
let callback;
@@ -171,6 +171,6 @@ export function stopMonitoring(): void {
callback = null;
}
} else {
androidUnregisterBroadcastReceiver(android.net.ConnectivityManager.CONNECTIVITY_ACTION);
Application.android.unregisterBroadcastReceiver(android.net.ConnectivityManager.CONNECTIVITY_ACTION);
}
}

View File

@@ -3,7 +3,6 @@
// Init globals first (use import to ensure it's always at the top)
import './globals';
export * from './application';
export { androidRegisterBroadcastReceiver, androidUnregisterBroadcastReceiver, androidRegisteredReceivers, iosAddNotificationObserver, iosRemoveNotificationObserver, iosNotificationObservers } from './application/helpers';
export { getNativeApp, setNativeApp } from './application/helpers-common';
export * as ApplicationSettings from './application-settings';
import * as Accessibility from './accessibility';