From 6f531587c59422459712bb65e548cf50289ff1e9 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Wed, 5 Nov 2025 05:06:48 -0400 Subject: [PATCH 1/3] feat(android): new OnBackPressed handling --- packages/core/ui/core/view/index.android.ts | 67 ++++++++++++++++- packages/core/ui/frame/index.android.ts | 82 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 74fc92bde6..202a027d49 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -26,6 +26,8 @@ import * as Utils from '../../../utils'; import { SDK_VERSION } from '../../../utils/constants'; import { BoxShadow } from '../../styling/box-shadow'; import { NativeScriptAndroidView } from '../../utils'; +import { Device } from '../../../platform'; +import { Application } from '../../../application/application'; export * from './view-common'; // helpers (these are okay re-exported here) @@ -72,6 +74,55 @@ interface DialogOptions { dismissCallback: () => void; } +let OnBackPressedCallback; + +if (parseInt(Device.sdkVersion) >= 33) { + OnBackPressedCallback = (androidx.activity.OnBackPressedCallback).extend({ + handleOnBackPressed() { + console.log('OnBackPressedCallback handleOnBackPressed called'); + const dialog = this['_dialog']?.get(); + + if (!dialog) { + // disable the callback and call super to avoid infinite loop + + this.setEnabled(false); + + return; + } + + const view = dialog.fragment.owner; + + const args = { + eventName: 'activityBackPressed', + + object: view, + + activity: view._context, + + cancel: false, + }; + + // Fist fire application.android global event + + Application.android.notify(args); + + if (args.cancel) { + return; + } + + view.notify(args); + + if (!args.cancel) { + this.setEnabled(false); + + dialog.getOnBackPressedDispatcher().onBackPressed(); + + this.setEnabled(true); + } + }, + }); +} + interface TouchListener { new (owner: View): android.view.View.OnTouchListener; } @@ -121,7 +172,7 @@ function initializeDialogFragment() { } @NativeClass - class DialogImpl extends android.app.Dialog { + class DialogImpl extends androidx.appcompat.app.AppCompatDialog { constructor( public fragment: DialogFragmentImpl, context: android.content.Context, @@ -129,6 +180,16 @@ function initializeDialogFragment() { ) { super(context, themeResId); + if (parseInt(Device.sdkVersion) >= 33 && OnBackPressedCallback) { + const callback = new OnBackPressedCallback(true); + + callback['_dialog'] = new WeakRef(this); + + // @ts-ignore + + this.getOnBackPressedDispatcher().addCallback(this, callback); + } + return global.__native(this); } @@ -138,6 +199,10 @@ function initializeDialogFragment() { } public onBackPressed(): void { + if (parseInt(Device.sdkVersion) >= 33) { + super.onBackPressed(); + return; + } const view = this.fragment.owner; const args = { eventName: 'activityBackPressed', diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index a8838b7e8d..168c81ac52 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -17,6 +17,7 @@ import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, import { Application } from '../../application/application'; import { isEmbedded, setEmbeddedView } from '../embedding'; import { CALLBACKS, FRAMEID, framesCache, setFragmentCallbacks } from './frame-helper-for-android'; +import { Device } from '../../platform'; export * from './frame-common'; export { setFragmentClass } from './fragment'; @@ -762,6 +763,78 @@ function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frame activity.startActivity(intent); } +let OnBackPressedCallback; + +if (parseInt(Device.sdkVersion) >= 33) { + OnBackPressedCallback = (androidx.activity.OnBackPressedCallback).extend('com.tns.OnBackPressedCallback', { + handleOnBackPressed() { + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onBackPressed;', Trace.categories.NativeLifecycle); + } + + const activity = this['_activity']?.get(); + + if (!activity) { + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onBackPressed; Activity is null, calling super', Trace.categories.NativeLifecycle); + } + + this.setEnabled(false); + + return; + } + + const args = { + eventName: 'activityBackPressed', + + object: Application, + + android: Application.android, + + activity: activity, + + cancel: false, + }; + + Application.android.notify(args); + + if (args.cancel) { + return; + } + + const view = activity._rootView; + + let callSuper = false; + + const viewArgs = { + eventName: 'activityBackPressed', + + object: view, + + activity: activity, + + cancel: false, + }; + + view?.notify(viewArgs); + + // In the case of Frame, use this callback only if it was overridden, since the original will cause navigation issues + + if (!viewArgs.cancel && (view?.onBackPressed === Frame.prototype.onBackPressed || !view?.onBackPressed())) { + callSuper = view instanceof Frame ? !Frame.goBack() : true; + } + + if (callSuper) { + this.setEnabled(false); + + activity.getOnBackPressedDispatcher().onBackPressed(); + + this.setEnabled(true); + } + }, + }); +} + const activityRootViewsMap = new Map>(); const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId'; @@ -1055,4 +1128,13 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks export function setActivityCallbacks(activity: androidx.appcompat.app.AppCompatActivity): void { activity[CALLBACKS] = new ActivityCallbacksImplementation(); + if (OnBackPressedCallback && !activity['_onBackPressed']) { + const callback = new OnBackPressedCallback(true); + + callback['_activity'] = new WeakRef(activity); + + activity['_onBackPressed'] = true; + + (activity as androidx.activity.ComponentActivity).getOnBackPressedDispatcher().addCallback(activity, callback); + } } From eb6d43719b96d735ab1bcebbd29884c390016819 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 5 Nov 2025 10:36:44 -0800 Subject: [PATCH 2/3] chore: remove deep application import --- packages/core/references.d.ts | 2 +- packages/core/ui/core/view/index.android.ts | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/core/references.d.ts b/packages/core/references.d.ts index db2b50e87e..4300b64aee 100644 --- a/packages/core/references.d.ts +++ b/packages/core/references.d.ts @@ -4,7 +4,7 @@ /// /// /// -/// +/// /// /// /// diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 202a027d49..40f456713e 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -27,7 +27,6 @@ import { SDK_VERSION } from '../../../utils/constants'; import { BoxShadow } from '../../styling/box-shadow'; import { NativeScriptAndroidView } from '../../utils'; import { Device } from '../../../platform'; -import { Application } from '../../../application/application'; export * from './view-common'; // helpers (these are okay re-exported here) @@ -77,7 +76,7 @@ interface DialogOptions { let OnBackPressedCallback; if (parseInt(Device.sdkVersion) >= 33) { - OnBackPressedCallback = (androidx.activity.OnBackPressedCallback).extend({ + OnBackPressedCallback = (androidx.activity.OnBackPressedCallback as any).extend({ handleOnBackPressed() { console.log('OnBackPressedCallback handleOnBackPressed called'); const dialog = this['_dialog']?.get(); @@ -92,19 +91,15 @@ if (parseInt(Device.sdkVersion) >= 33) { const view = dialog.fragment.owner; - const args = { + const args: AndroidActivityBackPressedEventData = { eventName: 'activityBackPressed', - object: view, - activity: view._context, - cancel: false, }; // Fist fire application.android global event - - Application.android.notify(args); + getNativeScriptGlobals().events.notify(args); if (args.cancel) { return; From 931cf89c11787f41f93577a8d7f45afc99bd5df4 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 5 Nov 2025 10:38:47 -0800 Subject: [PATCH 3/3] chore: reduce another potential circ import to deeper constant usage --- packages/core/ui/core/view/index.android.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 40f456713e..aa83cfaf64 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -26,7 +26,6 @@ import * as Utils from '../../../utils'; import { SDK_VERSION } from '../../../utils/constants'; import { BoxShadow } from '../../styling/box-shadow'; import { NativeScriptAndroidView } from '../../utils'; -import { Device } from '../../../platform'; export * from './view-common'; // helpers (these are okay re-exported here) @@ -75,7 +74,7 @@ interface DialogOptions { let OnBackPressedCallback; -if (parseInt(Device.sdkVersion) >= 33) { +if (SDK_VERSION >= 33) { OnBackPressedCallback = (androidx.activity.OnBackPressedCallback as any).extend({ handleOnBackPressed() { console.log('OnBackPressedCallback handleOnBackPressed called'); @@ -175,7 +174,7 @@ function initializeDialogFragment() { ) { super(context, themeResId); - if (parseInt(Device.sdkVersion) >= 33 && OnBackPressedCallback) { + if (SDK_VERSION >= 33 && OnBackPressedCallback) { const callback = new OnBackPressedCallback(true); callback['_dialog'] = new WeakRef(this); @@ -194,7 +193,7 @@ function initializeDialogFragment() { } public onBackPressed(): void { - if (parseInt(Device.sdkVersion) >= 33) { + if (SDK_VERSION >= 33) { super.onBackPressed(); return; }