diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 1f400277e..cd0bd4166 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -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 = {}; + private _registeredReceiversById: Record = {}; + private _nextReceiverId: number = 1; + private _pendingReceiverRegistrations: Omit[] = []; 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) { diff --git a/packages/core/application/application.d.ts b/packages/core/application/application.d.ts index 8aaaa528b..e5c2eec0c 100644 --- a/packages/core/application/application.d.ts +++ b/packages/core/application/application.d.ts @@ -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; diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index 761145c05..101823304 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -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 = 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; @@ -243,6 +262,8 @@ export class iOSApplication extends ApplicationCommon { private _primaryScene: UIWindowScene | null = null; private _openedScenesById = new Map(); + 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' { diff --git a/packages/core/application/helpers.android.ts b/packages/core/application/helpers.android.ts index 79b2c3abb..04292707a 100644 --- a/packages/core/application/helpers.android.ts +++ b/packages/core/application/helpers.android.ts @@ -26,68 +26,6 @@ function getApplicationContext(): android.content.Context { return getNativeApp().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 = []; -export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: any) => void) {} -export function iosRemoveNotificationObserver(observer: any, notificationName: string) {} diff --git a/packages/core/application/helpers.d.ts b/packages/core/application/helpers.d.ts index 1d938c199..d99f4e633 100644 --- a/packages/core/application/helpers.d.ts +++ b/packages/core/application/helpers.d.ts @@ -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; diff --git a/packages/core/application/helpers.ios.ts b/packages/core/application/helpers.ios.ts index a435527b4..0d28a66e3 100644 --- a/packages/core/application/helpers.ios.ts +++ b/packages/core/application/helpers.ios.ts @@ -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 = 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) { diff --git a/packages/core/connectivity/index.android.ts b/packages/core/connectivity/index.android.ts index 1a61ddcad..a02981b11 100644 --- a/packages/core/connectivity/index.android.ts +++ b/packages/core/connectivity/index.android.ts @@ -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); } } diff --git a/packages/core/index.ts b/packages/core/index.ts index a50e888d3..4e12c9fe9 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -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';