Skip to content

Commit 1985bf8

Browse files
committed
fix(android): keep the root view when an activity replaces another
Android can start a new activity before the old one stops. When both activities use one root view, the old activity unloaded and destroyed the view that the new activity showed. The view also kept the old FragmentManager, so the screen stayed blank. onStop, onDestroy, and onSaveInstanceState now act only on a root view that the activity shows. An activity takes the view back when it starts again. A new activity with no content source inherits the root view of a finishing window. startActivity moves to a surviving activity only when the destroyed activity is finishing. A back press with no root view now goes to the system, so it can close an activity during an async startup. A reopen after exit still needs launch to fire again. This change does not cover that case.
1 parent 0f5c41f commit 1985bf8

2 files changed

Lines changed: 78 additions & 33 deletions

File tree

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function initNativeScriptLifecycleCallbacks() {
134134
}
135135

136136
if (activity === this.nativescriptActivity) {
137-
this.nativescriptActivity = undefined;
137+
this.nativescriptActivity = Application.android._findSurvivingNativeScriptActivity(activity);
138138
}
139139

140140
if (activity === Application.android.startActivity) {
@@ -733,6 +733,17 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp
733733
return this._windows.find((nw) => nw.android?.activity === activity) as AndroidNativeWindow | undefined;
734734
}
735735

736+
_findSurvivingNativeScriptActivity(destroyed: androidx.appcompat.app.AppCompatActivity): androidx.appcompat.app.AppCompatActivity | undefined {
737+
if (!destroyed.isFinishing()) {
738+
return undefined;
739+
}
740+
741+
return this._getWindows()
742+
.filter((nativeWindow) => nativeWindow.state === 'attached')
743+
.map((nativeWindow) => (nativeWindow as AndroidNativeWindow).activity)
744+
.find((candidate) => candidate && candidate !== destroyed && !candidate.isFinishing() && 'isNativeScriptActivity' in candidate);
745+
}
746+
736747
/**
737748
* @internal - Feeds a window's active state into the application-level 'resume'/'suspend'
738749
* events, which describe the app as a whole: they are raised when the first window becomes

‎packages/core/ui/frame/index.android.ts‎

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -752,25 +752,22 @@ if (SDK_VERSION >= 33) {
752752
}
753753

754754
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
755-
let callSuper: boolean = false;
756-
757-
if (callbacks) {
758-
const view = callbacks.getRootView();
759-
760-
if (view) {
761-
const viewArgs = <AndroidActivityBackPressedEventData>{
762-
eventName: NativeWindowEvents.activityBackPressed,
763-
object: view,
764-
activity: activity,
765-
cancel: false,
766-
};
767-
768-
view.notify(viewArgs);
769-
770-
// In the case of Frame, use this callback only if it was overridden, since the original will cause navigation issues
771-
if (!viewArgs.cancel && (view.onBackPressed === Frame.prototype.onBackPressed || !view.onBackPressed())) {
772-
callSuper = view instanceof Frame ? !Frame.goBack(Frame.topmost(nativeWindow)) : true;
773-
}
755+
const view = callbacks?.getRootView();
756+
let callSuper: boolean = !view;
757+
758+
if (view) {
759+
const viewArgs = <AndroidActivityBackPressedEventData>{
760+
eventName: NativeWindowEvents.activityBackPressed,
761+
object: view,
762+
activity: activity,
763+
cancel: false,
764+
};
765+
766+
view.notify(viewArgs);
767+
768+
// In the case of Frame, use this callback only if it was overridden, since the original will cause navigation issues
769+
if (!viewArgs.cancel && (view.onBackPressed === Frame.prototype.onBackPressed || !view.onBackPressed())) {
770+
callSuper = view instanceof Frame ? !Frame.goBack(Frame.topmost(nativeWindow)) : true;
774771
}
775772
}
776773

@@ -881,7 +878,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
881878
@profile
882879
public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void {
883880
superFunc.call(activity, outState);
884-
const rootView = this._rootView;
881+
const rootView = this.getRootViewShownBy(activity);
885882
if (rootView instanceof Frame) {
886883
outState.putInt(INTENT_EXTRA, rootView.android.frameId);
887884
rootView._saveFragmentsState();
@@ -926,6 +923,10 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
926923
}
927924

928925
const rootView = this._rootView;
926+
if (rootView && rootView._context !== activity && !isEmbedded()) {
927+
this.setActivityContent(activity, null, false);
928+
}
929+
929930
if (rootView && !rootView.isLoaded && !isEmbedded()) {
930931
rootView.callLoaded();
931932
}
@@ -939,7 +940,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
939940
Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle);
940941
}
941942

942-
const rootView = this._rootView;
943+
const rootView = this.getRootViewShownBy(activity);
943944
if (rootView && rootView.isLoaded && !isEmbedded()) {
944945
rootView.callUnloaded();
945946
}
@@ -970,7 +971,7 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
970971
Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle);
971972
}
972973

973-
const rootView = this._rootView;
974+
const rootView = this.getRootViewShownBy(activity);
974975
if (rootView) {
975976
rootView._tearDownUI(true);
976977
}
@@ -1009,6 +1010,12 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
10091010
}
10101011

10111012
const view = this._rootView;
1013+
if (!view) {
1014+
superFunc.call(activity);
1015+
1016+
return;
1017+
}
1018+
10121019
let callSuper = false;
10131020

10141021
const viewArgs = <AndroidActivityBackPressedEventData>{
@@ -1088,6 +1095,30 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
10881095
Application.android.notify(resultArgs);
10891096
}
10901097

1098+
private getRootViewShownBy(activity: any): View | undefined {
1099+
return this._rootView?._context === activity ? this._rootView : undefined;
1100+
}
1101+
1102+
private getRootViewOfFinishingWindow(activity: any): View | undefined {
1103+
if (Application.getWindowContentResolver()) {
1104+
return undefined;
1105+
}
1106+
1107+
const finishing = Application.android._getWindows().find((nativeWindow) => {
1108+
const candidate = (nativeWindow as AndroidNativeWindow).activity;
1109+
1110+
return candidate && candidate !== activity && candidate.isFinishing() && nativeWindow.rootView;
1111+
});
1112+
1113+
return finishing?.rootView;
1114+
}
1115+
1116+
private unloadFromPreviousActivity(rootView: View, activity: any): void {
1117+
if (rootView.isLoaded && rootView._context !== activity && !isEmbedded()) {
1118+
rootView.callUnloaded();
1119+
}
1120+
}
1121+
10911122
public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity, view?: View): void {
10921123
if (this._rootView) {
10931124
const manager = this._rootView._getFragmentManager();
@@ -1139,16 +1170,17 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
11391170
// This method installs the root view on the activity itself, so the resolved view is
11401171
// handed back rather than applied through NativeWindow.setContent(), which would
11411172
// re-enter here through resetActivityContent().
1142-
rootView = Application._resolveWindowContent(
1143-
nativeWindow,
1144-
{
1145-
window: nativeWindow,
1146-
isPrimary: nativeWindow.isPrimary,
1147-
data: dataDeserialize(intent?.getExtras()) ?? undefined,
1148-
android: { intent, savedInstanceState },
1149-
},
1150-
{ install: false, launchData },
1151-
);
1173+
rootView =
1174+
Application._resolveWindowContent(
1175+
nativeWindow,
1176+
{
1177+
window: nativeWindow,
1178+
isPrimary: nativeWindow.isPrimary,
1179+
data: dataDeserialize(intent?.getExtras()) ?? undefined,
1180+
android: { intent, savedInstanceState },
1181+
},
1182+
{ install: false, launchData },
1183+
) ?? this.getRootViewOfFinishingWindow(activity);
11521184
} else {
11531185
rootView = Application.createRootView(rootView, fireLaunchEvent, launchData);
11541186
}
@@ -1160,6 +1192,8 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks
11601192

11611193
activityRootViewsMap.set(rootView._domId, new WeakRef(rootView));
11621194

1195+
this.unloadFromPreviousActivity(rootView, activity);
1196+
11631197
// setup view as styleScopeHost
11641198
rootView._setupAsRootView(activity);
11651199

0 commit comments

Comments
 (0)