Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions packages/core/ui/frame/frame-helper-for-android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Fr
entry = current;
} else if (executingContext && executingContext.entry && executingContext.entry.fragmentTag === fragmentTag) {
entry = executingContext.entry;
} else {
// Android also restores fragments that were only in the backstack or still queued, so
// widen the lookup before treating this fragment as an orphan.
entry = frame._findEntryForTag(fragmentTag);
}

let page: Page;
Expand All @@ -52,7 +56,41 @@ function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Fr
entry.fragment = fragment;
_updateTransitions(entry);
} else {
throw new Error(`Could not find a page for ${fragmentTag}.`);
// Android restored a fragment no live entry owns anymore - the frame navigated on (or was
// reset) while the activity was being recreated. Throwing is fatal across the JNI boundary
// and this fragment can never produce a view, so discard it instead.
Trace.write(`Could not find a page for ${fragmentTag}. Discarding orphaned fragment.`, Trace.categories.NativeLifecycle, Trace.messageType.error);
removeFragmentIfAdded(fragment);
Comment on lines +62 to +63
}
}

/**
* Drops a fragment the JS side no longer owns from its FragmentManager, so the manager stops
* driving it through the lifecycle. The removal is always deferred - callers may be inside a
* FragmentManager transaction, where commitNow throws "already executing transactions".
*/
export function removeFragmentIfAdded(fragment: androidx.fragment.app.Fragment): void {
if (!fragment.isAdded()) {
return;
}

const manager = fragment.getParentFragmentManager();
if (manager.isDestroyed()) {
return;
}

manager.beginTransaction().remove(fragment).commitAllowingStateLoss();
}

/**
* Breaks the fragment -> BackstackEntry link so a fragment the FragmentManager is still holding
* cannot bring a discarded entry (and its torn down page) back into the frame.
*/
export function detachFragmentCallbacks(fragment: androidx.fragment.app.Fragment): void {
const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS];
if (callbacks) {
callbacks.entry = null;
callbacks.frame = null;
Comment on lines +92 to +93
}
}

Expand Down Expand Up @@ -214,7 +252,7 @@ export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks
const hasRemovingParent = fragment.getRemovingParentFragment();

if (hasRemovingParent) {
const nativeFrameView = this.frame.nativeViewProtected;
const nativeFrameView = this.frame?.nativeViewProtected;
if (nativeFrameView) {
const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(getNativeApp<android.app.Application>().getApplicationContext().getResources(), this.backgroundBitmap);
this.frame._originalBackground = this.frame.backgroundColor || new Color('White');
Expand All @@ -237,7 +275,10 @@ export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks

const entry = this.entry;
if (!entry) {
Trace.error(`${fragment}.onDestroy: entry is null or undefined`);
// A fragment that was never bound to an entry, or whose entry has already been
// discarded, is still destroyed by the FragmentManager. Trace.error routes to the error
// handler, which rethrows and turns this teardown into a fatal exception.
Trace.write(`${fragment}.onDestroy: entry is null or undefined`, Trace.categories.NativeLifecycle, Trace.messageType.error);

return null;
}
Expand Down Expand Up @@ -270,7 +311,7 @@ export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks
const hasRemovingParent = fragment.getRemovingParentFragment();

if (hasRemovingParent) {
this.backgroundBitmap = this.loadBitmapFromView(this.frame.nativeViewProtected);
this.backgroundBitmap = this.loadBitmapFromView(this.frame?.nativeViewProtected);
}
} finally {
superFunc.call(fragment);
Expand All @@ -279,7 +320,16 @@ export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks

@profile
public onResume(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
const frame = this.entry.resolvedPage.frame;
const frame = this.entry?.resolvedPage?.frame;
if (!frame) {
// Stale fragment the FragmentManager is still driving after its entry (or page) was
// discarded: there is no navigation left to complete, and dereferencing the missing page
// would throw across the JNI boundary.
superFunc.call(fragment);

return;
}

// on some cases during the first navigation on nested frames the animation doesn't trigger
// we depend on the animation (even None animation) to set the entry as the current entry
// animation should start between start and resume, so if we have an executing navigation here it probably means the animation was skipped
Expand All @@ -288,7 +338,7 @@ export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks
const weakRef = new WeakRef(this);
setTimeout(() => {
const owner = weakRef.get();
if (!owner) {
if (!owner || !owner.entry) {
return;
}
if (!owner.entry.isAnimationRunning) {
Expand Down
14 changes: 12 additions & 2 deletions packages/core/ui/frame/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getAppMainEntry } from '../../application/helpers-common';
import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData } from '../../application/application-interfaces';
import { Application } from '../../application/application';
import { isEmbedded, setEmbeddedView } from '../embedding';
import { CALLBACKS, FRAMEID, framesCache, setFragmentCallbacks } from './frame-helper-for-android';
import { CALLBACKS, detachFragmentCallbacks, FRAMEID, framesCache, removeFragmentIfAdded, setFragmentCallbacks } from './frame-helper-for-android';
import { SDK_VERSION } from '../../utils';

export * from './frame-common';
Expand Down Expand Up @@ -459,8 +459,18 @@ export class Frame extends FrameBase {
public _removeEntry(removed: BackstackEntry): void {
super._removeEntry(removed);

if (removed.fragment) {
const fragment = removed.fragment;
if (fragment) {
_clearEntry(removed);

// The entry is gone now - its page was torn down and resolvedPage cleared - but the
// fragment can still be sitting in the FragmentManager: navigation only evicts fragments
// sharing the frame's current container, so anything left over from an activity
// recreation or a frame reset survives. Cut it loose here, or the FragmentManager keeps
// driving it through the lifecycle against a dead entry.
detachFragmentCallbacks(fragment);
removeFragmentIfAdded(fragment);

removed.fragment = null;
}

Expand Down
Loading