// Definitions. import { AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationTransition, AndroidFragmentCallbacks, AndroidActivityCallbacks } from "."; import { Page } from "../page"; // Types. import { FrameBase, application, NavigationContext, stack, goBack, View, Observable, traceEnabled, traceWrite, traceCategories } from "./frame-common"; import { DIALOG_FRAGMENT_TAG } from "../page/constants"; import { _setAndroidFragmentTransitions, _waitForAnimationEnd, _onFragmentCreateAnimator, _updateAnimationFragment } from "./fragment.transitions"; import { profile } from "../../profiling"; export * from "./frame-common"; const HIDDEN = "_hidden"; const INTENT_EXTRA = "com.tns.activity"; const FRAMEID = "_frameId"; const CALLBACKS = "_callbacks"; let navDepth = -1; let fragmentId = -1; let activityInitialized: boolean; export class Frame extends FrameBase { private _android: AndroidFrame; private _delayedNavigationEntry: BackstackEntry; private _containerViewId: number = -1; private _isBack: boolean = true; constructor() { super(); this._android = new AndroidFrame(this); } public static get defaultAnimatedNavigation(): boolean { return FrameBase.defaultAnimatedNavigation; } public static set defaultAnimatedNavigation(value: boolean) { FrameBase.defaultAnimatedNavigation = value; } public static get defaultTransition(): NavigationTransition { return FrameBase.defaultTransition; } public static set defaultTransition(value: NavigationTransition) { FrameBase.defaultTransition = value; } get containerViewId(): number { return this._containerViewId; } get android(): AndroidFrame { return this._android; } private createFragment(backstackEntry: BackstackEntry, fragmentTag: string): android.app.Fragment { ensureFragmentClass(); const newFragment: android.app.Fragment = new fragmentClass(); const args = new android.os.Bundle(); args.putInt(FRAMEID, this._android.frameId); newFragment.setArguments(args); setFragmentCallbacks(newFragment); const callbacks = newFragment[CALLBACKS]; callbacks.frame = this; callbacks.entry = backstackEntry; // backstackEntry backstackEntry.fragmentTag = fragmentTag; backstackEntry.navDepth = navDepth; return newFragment; } public setCurrent(entry: BackstackEntry): void { this.changeCurrentPage(entry); this._currentEntry = entry; this._isBack = true; this._processNavigationQueue(entry.resolvedPage); } private changeCurrentPage(entry: BackstackEntry) { const isBack = this._isBack; let page: Page = this.currentPage; if (page) { if (page.frame === this && isBack) { this._removeView(page); } if (page.isLoaded) { // Forward navigation does not remove page from frame so we raise unloaded manually. page.onUnloaded(); } page.onNavigatedFrom(isBack); } const newPage = entry.resolvedPage; newPage._fragmentTag = entry.fragmentTag; this._currentEntry = entry; newPage.onNavigatedTo(isBack); } @profile public _navigateCore(backstackEntry: BackstackEntry) { super._navigateCore(backstackEntry); this._isBack = false; const activity = this._android.activity; if (!activity) { // Activity not associated. In this case we have two execution paths: // 1. This is the main frame for the application // 2. This is an inner frame which requires a new Activity const currentActivity = this._android.currentActivity; if (currentActivity) { startActivity(currentActivity, this._android.frameId); } this._delayedNavigationEntry = backstackEntry; return; } const manager = activity.getFragmentManager(); // Current Fragment const currentFragment = this._currentEntry ? manager.findFragmentByTag(this._currentEntry.fragmentTag) : null; const clearHistory = backstackEntry.entry.clearHistory; // New Fragment if (clearHistory) { navDepth = -1; } navDepth++; fragmentId++; const newFragmentTag = `fragment${fragmentId}[${navDepth}]`; const newFragment = this.createFragment(backstackEntry, newFragmentTag); const fragmentTransaction = manager.beginTransaction(); const animated = this._getIsAnimatedNavigation(backstackEntry.entry); const navigationTransition = this._getNavigationTransition(backstackEntry.entry); _setAndroidFragmentTransitions(animated, navigationTransition, currentFragment, newFragment, fragmentTransaction, manager); if (currentFragment) { if (clearHistory) { removeFragments(manager, fragmentTransaction); } else { fragmentTransaction.addToBackStack(this._currentEntry.fragmentTag); } if (animated && !navigationTransition) { fragmentTransaction.setTransition(android.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); } } fragmentTransaction.replace(this.containerViewId, newFragment, newFragmentTag); fragmentTransaction.commit(); if (clearHistory) { manager.popBackStackImmediate(null, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } } public _goBackCore(backstackEntry: BackstackEntry) { super._goBackCore(backstackEntry); navDepth = backstackEntry.navDepth; const manager = this._android.activity.getFragmentManager(); if (manager.getBackStackEntryCount() > 0) { const fragmentInBackstack = manager.findFragmentByTag(backstackEntry.fragmentTag); const currentFragment = manager.findFragmentByTag(this._currentEntry.fragmentTag); _waitForAnimationEnd(fragmentInBackstack, currentFragment); // pop all other fragments up until the named one // this handles cases where user may navigate to an inner page without adding it on the backstack manager.popBackStack(backstackEntry.fragmentTag, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } } public createNativeView() { const root = new org.nativescript.widgets.ContentLayout(this._context); if (this._containerViewId < 0) { this._containerViewId = android.view.View.generateViewId(); } return root; } public initNativeView(): void { super.initNativeView(); this._android.rootViewGroup = this.nativeViewProtected; this._android.rootViewGroup.setId(this._containerViewId); } public disposeNativeView() { // we should keep the reference to underlying native object, since frame can contain many pages. this._android.rootViewGroup = null; super.disposeNativeView(); } public _popFromFrameStack() { if (!this._isInFrameStack) { return; } super._popFromFrameStack(); if (this._android.hasOwnActivity) { this._android.activity.finish(); } } public _printNativeBackStack() { if (!this._android.activity) { return; } const manager = this._android.activity.getFragmentManager(); const length = manager.getBackStackEntryCount(); let i = length - 1; console.log(`Fragment Manager Back Stack: `); while (i >= 0) { const fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); console.log(`\t${fragment}`); } } public _getNavBarVisible(page: Page): boolean { if (page.actionBarHidden !== undefined) { return !page.actionBarHidden; } if (this._android && this._android.showActionBar !== undefined) { return this._android.showActionBar; } return true; } protected _processNavigationContext(navigationContext: NavigationContext) { let activity = this._android.activity; if (activity) { let isForegroundActivity = activity === application.android.foregroundActivity; let isPaused = application.android.paused; if (activity && !isForegroundActivity || (isForegroundActivity && isPaused)) { let weakActivity = new WeakRef(activity); let resume = (args: application.AndroidActivityEventData) => { let weakActivityInstance = weakActivity.get(); let isCurrent = args.activity === weakActivityInstance; if (!weakActivityInstance) { if (traceEnabled()) { traceWrite(`Frame _processNavigationContext: Drop For Activity GC-ed`, traceCategories.Navigation); } unsubscribe(); return; } if (isCurrent) { if (traceEnabled()) { traceWrite(`Frame _processNavigationContext: Activity.Resumed, Continue`, traceCategories.Navigation); } super._processNavigationContext(navigationContext); unsubscribe(); } }; let unsubscribe = () => { if (traceEnabled()) { traceWrite(`Frame _processNavigationContext: Unsubscribe from Activity.Resumed`, traceCategories.Navigation); } application.android.off(application.AndroidApplication.activityResumedEvent, resume); application.android.off(application.AndroidApplication.activityStoppedEvent, unsubscribe); application.android.off(application.AndroidApplication.activityDestroyedEvent, unsubscribe); }; if (traceEnabled()) { traceWrite(`Frame._processNavigationContext: Subscribe for Activity.Resumed`, traceCategories.Navigation); } application.android.on(application.AndroidApplication.activityResumedEvent, resume); application.android.on(application.AndroidApplication.activityStoppedEvent, unsubscribe); application.android.on(application.AndroidApplication.activityDestroyedEvent, unsubscribe); return; } } super._processNavigationContext(navigationContext); } } function removeFragments(manager: android.app.FragmentManager, ft: android.app.FragmentTransaction): void { if (traceEnabled()) { traceWrite(`CLEAR HISTORY`, traceCategories.Navigation); } for (let i = manager.getBackStackEntryCount() - 1; i >= 0; i--) { const fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i).getName()); ft.detach(fragment); const page = fragment[CALLBACKS].entry.resolvedPage; if (page.frame) { page.frame._removeView(page); } } } let framesCounter = 0; let framesCache: Array> = new Array>(); class AndroidFrame extends Observable implements AndroidFrameDefinition { public rootViewGroup: android.view.ViewGroup; public hasOwnActivity = false; public frameId; private _showActionBar = true; private _owner: Frame; public cachePagesOnNavigate: boolean = true; constructor(owner: Frame) { super(); this._owner = owner; this.frameId = framesCounter++; framesCache.push(new WeakRef(this)); } public get showActionBar(): boolean { return this._showActionBar; } public set showActionBar(value: boolean) { if (this._showActionBar !== value) { this._showActionBar = value; if (this.owner.currentPage) { this.owner.currentPage.actionBar.update(); } } } public get activity(): android.app.Activity { let activity: android.app.Activity = this.owner._context; if (activity) { return activity; } // traverse the parent chain for an ancestor Frame let currView = this._owner.parent; while (currView) { if (currView instanceof Frame) { return (currView).android.activity; } currView = currView.parent; } return undefined; } public get actionBar(): android.app.ActionBar { let activity = this.currentActivity; if (!activity) { return undefined; } let bar = activity.getActionBar(); if (!bar) { return undefined; } return bar; } public get currentActivity(): android.app.Activity { let activity = this.activity; if (activity) { return activity; } let frames = stack(); for (let length = frames.length, i = length - 1; i >= 0; i--) { activity = frames[i].android.activity; if (activity) { return activity; } } return undefined; } public get owner(): Frame { return this._owner; } public canGoBack() { if (!this.activity) { return false; } // can go back only if it is not the main one. return this.activity.getIntent().getAction() !== android.content.Intent.ACTION_MAIN; } public fragmentForPage(page: Page): any { if (!page) { return undefined; } let tag = page._fragmentTag; if (tag) { let manager = this.activity.getFragmentManager(); return manager.findFragmentByTag(tag); } return undefined; } } function findPageForFragment(fragment: android.app.Fragment, frame: Frame) { const fragmentTag = fragment.getTag(); if (traceEnabled()) { traceWrite(`Finding page for ${fragmentTag}.`, traceCategories.NativeLifecycle); } if (fragmentTag === DIALOG_FRAGMENT_TAG) { return; } const entry = frame._findEntryForTag(fragmentTag); const page = entry ? entry.resolvedPage : undefined; if (page) { const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS]; callbacks.frame = frame; callbacks.entry = entry; _updateAnimationFragment(fragment); } else { throw new Error(`Could not find a page for ${fragmentTag}.`); } } function startActivity(activity: android.app.Activity, frameId: number) { // TODO: Implicitly, we will open the same activity type as the current one const intent = new android.content.Intent(activity, activity.getClass()); intent.setAction(android.content.Intent.ACTION_DEFAULT); intent.putExtra(INTENT_EXTRA, frameId); // TODO: Put the navigation context (if any) in the intent activity.startActivity(intent); } function getFrameById(frameId: number): Frame { // Find the frame for this activity. for (let i = 0; i < framesCache.length; i++) { let aliveFrame = framesCache[i].get(); if (aliveFrame && aliveFrame.frameId === frameId) { return aliveFrame.owner; } } return null; } function ensureFragmentClass() { if (fragmentClass) { return; } // this require will apply the FragmentClass implementation require("ui/frame/fragment"); if (!fragmentClass) { throw new Error("Failed to initialize the extended android.app.Fragment class"); } } let fragmentClass: any; export function setFragmentClass(clazz: any) { if (fragmentClass) { throw new Error("Fragment class already initialized"); } fragmentClass = clazz; } class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { public frame: Frame; public entry: BackstackEntry; @profile public onHiddenChanged(fragment: android.app.Fragment, hidden: boolean, superFunc: Function): void { if (traceEnabled()) { traceWrite(`${fragment}.onHiddenChanged(${hidden})`, traceCategories.NativeLifecycle); } superFunc.call(fragment, hidden); } @profile public onCreateAnimator(fragment: android.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { let nextAnimString: string; switch (nextAnim) { case -10: nextAnimString = "enter"; break; case -20: nextAnimString = "exit"; break; case -30: nextAnimString = "popEnter"; break; case -40: nextAnimString = "popExit"; break; } let animator = _onFragmentCreateAnimator(fragment, nextAnim); if (!animator) { animator = superFunc.call(fragment, transit, enter, nextAnim); } if (traceEnabled()) { traceWrite(`${fragment}.onCreateAnimator(${transit}, ${enter ? "enter" : "exit"}, ${nextAnimString}): ${animator ? 'animator' : 'no animator'}`, traceCategories.NativeLifecycle); } return animator; } @profile public onCreate(fragment: android.app.Fragment, savedInstanceState: android.os.Bundle, superFunc: Function): void { if (traceEnabled()) { traceWrite(`${fragment}.onCreate(${savedInstanceState})`, traceCategories.NativeLifecycle); } superFunc.call(fragment, savedInstanceState); // There is no entry set to the fragment, so this must be destroyed fragment that was recreated by Android. // We should find its corresponding page in our backstack and set it manually. if (!this.entry) { const frameId = fragment.getArguments().getInt(FRAMEID); const frame = getFrameById(frameId); if (!frame) { throw new Error(`Cannot find Frame for ${fragment}`); } findPageForFragment(fragment, frame); } } @profile public onCreateView(fragment: android.app.Fragment, inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle, superFunc: Function): android.view.View { if (traceEnabled()) { traceWrite(`${fragment}.onCreateView(inflater, container, ${savedInstanceState})`, traceCategories.NativeLifecycle); } const entry = this.entry; const page = entry.resolvedPage; try { if (savedInstanceState && savedInstanceState.getBoolean(HIDDEN, false)) { fragment.getFragmentManager().beginTransaction().hide(fragment).commit(); } const frame = this.frame; if (page.parent === frame) { if (frame.isLoaded && !page.isLoaded) { page.onLoaded(); } } else { this.frame._addView(page); } } catch (ex) { const label = new android.widget.TextView(container.getContext()); label.setText(ex.message + ", " + ex.stackTrace); return label; } return page.nativeViewProtected; } @profile public onSaveInstanceState(fragment: android.app.Fragment, outState: android.os.Bundle, superFunc: Function): void { if (traceEnabled()) { traceWrite(`${fragment}.onSaveInstanceState(${outState})`, traceCategories.NativeLifecycle); } superFunc.call(fragment, outState); if (fragment.isHidden()) { outState.putBoolean(HIDDEN, true); } } @profile public onDestroyView(fragment: android.app.Fragment, superFunc: Function): void { const entry = this.entry; const page = entry.resolvedPage; if (traceEnabled()) { traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle); } superFunc.call(fragment); } @profile public onDestroy(fragment: android.app.Fragment, superFunc: Function): void { const entry = this.entry; const page = entry.resolvedPage; superFunc.call(fragment); const frame = page.frame; // This will happen if we navigate forward then kill the app // Existing pages will be in the frame so we need to destory them. if (frame && !frame.isCurrent(entry)) { frame._removeView(page); } } @profile public toStringOverride(fragment: android.app.Fragment, superFunc: Function): string { return `${fragment.getTag()}<${this.entry ? this.entry.resolvedPage : ""}>`; } } class ActivityCallbacksImplementation implements AndroidActivityCallbacks { private _rootView: View; @profile public onCreate(activity: android.app.Activity, savedInstanceState: android.os.Bundle, superFunc: Function): void { if (traceEnabled()) { traceWrite(`Activity.onCreate(${savedInstanceState})`, traceCategories.NativeLifecycle); } const app = application.android; const intent = activity.getIntent(); let rootView = this.notifyLaunch(intent, savedInstanceState); let frameId = -1; const extras = intent.getExtras(); // We have extras when we call - new Frame().navigate(); // savedInstanceState is used when activity is recreated. // NOTE: On API 23+ we get extras on first run. // Check changed - first try to get frameId from Extras if not from saveInstanceState. if (extras) { frameId = extras.getInt(INTENT_EXTRA, -1); } if (savedInstanceState && frameId < 0) { frameId = savedInstanceState.getInt(INTENT_EXTRA, -1); } // If we have frameId from extras - we are starting a new activity from navigation (e.g. new Frame().navigate())) // Then we check if we have frameId from savedInstanceState - this happens when Activity is destroyed but app was not (e.g. suspend) // Only then we fallback to the view returned from the event. This is done in order to have backwards compatibility (remove it for 2.0.0). let frame: Frame; let navParam; if (frameId >= 0) { rootView = getFrameById(frameId); } if (!rootView) { navParam = application.getMainEntry(); if (navParam) { frame = new Frame(); } else { // TODO: Throw an exception? throw new Error("A Frame must be used to navigate to a Page."); } rootView = frame; } // If there is savedInstanceState this call will recreate all fragments that were previously in the navigation. // We take care of associating them with a Page from our backstack in the onAttachFragment callback. // If there is savedInstanceState and activityInitialized is false we are restarted but process was killed. // For now we treat it like first run (e.g. we are not passing savedInstanceState so no fragments are being restored). // When we add support for application save/load state - revise this logic. let isRestart = !!savedInstanceState && activityInitialized; superFunc.call(activity, isRestart ? savedInstanceState : null); this._rootView = rootView; // Initialize native visual tree; rootView._setupAsRootView(activity); activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams()); // frameId is negative w if (frame) { frame.navigate(navParam); } activityInitialized = true; } @profile private notifyLaunch(intent: android.content.Intent, savedInstanceState: android.os.Bundle): View { const launchArgs: application.LaunchEventData = { eventName: application.launchEvent, object: application.android, android: intent, savedInstanceState }; application.notify(launchArgs); return launchArgs.root; } @profile public onSaveInstanceState(activity: android.app.Activity, outState: android.os.Bundle, superFunc: Function): void { superFunc.call(activity, outState); let view = this._rootView; if (view instanceof Frame) { outState.putInt(INTENT_EXTRA, view.android.frameId); } } @profile public onStart(activity: any, superFunc: Function): void { superFunc.call(activity); if (traceEnabled()) { traceWrite("NativeScriptActivity.onStart();", traceCategories.NativeLifecycle); } let rootView = this._rootView; if (rootView && !rootView.isLoaded) { rootView.onLoaded(); } } @profile public onStop(activity: any, superFunc: Function): void { superFunc.call(activity); if (traceEnabled()) { traceWrite("NativeScriptActivity.onStop();", traceCategories.NativeLifecycle); } let rootView = this._rootView; if (rootView && rootView.isLoaded) { rootView.onUnloaded(); } } @profile public onDestroy(activity: any, superFunc: Function): void { let rootView = this._rootView; if (rootView && rootView._context) { rootView._tearDownUI(true); } superFunc.call(activity); if (traceEnabled()) { traceWrite("NativeScriptActivity.onDestroy();", traceCategories.NativeLifecycle); } const exitArgs = { eventName: application.exitEvent, object: application.android, android: activity }; application.notify(exitArgs); } @profile public onBackPressed(activity: any, superFunc: Function): void { if (traceEnabled()) { traceWrite("NativeScriptActivity.onBackPressed;", traceCategories.NativeLifecycle); } const args = { eventName: "activityBackPressed", object: application.android, activity: activity, cancel: false, }; application.android.notify(args); if (args.cancel) { return; } if (!goBack()) { superFunc.call(activity); } } @profile public onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array, grantResults: Array, superFunc: Function): void { if (traceEnabled()) { traceWrite("NativeScriptActivity.onRequestPermissionsResult;", traceCategories.NativeLifecycle); } application.android.notify({ eventName: "activityRequestPermissions", object: application.android, activity: activity, requestCode: requestCode, permissions: permissions, grantResults: grantResults }); } @profile public onActivityResult(activity: any, requestCode: number, resultCode: number, data: android.content.Intent, superFunc: Function): void { superFunc.call(activity, requestCode, resultCode, data); if (traceEnabled()) { traceWrite(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, traceCategories.NativeLifecycle); } application.android.notify({ eventName: "activityResult", object: application.android, activity: activity, requestCode: requestCode, resultCode: resultCode, intent: data }); } } export function setActivityCallbacks(activity: android.app.Activity): void { activity[CALLBACKS] = new ActivityCallbacksImplementation(); } export function setFragmentCallbacks(fragment: android.app.Fragment): void { fragment[CALLBACKS] = new FragmentCallbacksImplementation(); }