import definition = require("ui/frame"); import frameCommon = require("./frame-common"); import pages = require("ui/page"); import {View} from "ui/core/view"; import {Observable} from "data/observable"; import trace = require("trace"); import application = require("application"); import * as types from "utils/types"; import * as utils from "utils/utils"; import transitionModule = require("ui/transition"); global.moduleMerge(frameCommon, exports); var TAG = "_fragmentTag"; var HIDDEN = "_hidden"; var INTENT_EXTRA = "com.tns.activity"; var ROOT_VIEW = "_rootView"; var BACKSTACK_TAG = "_backstackTag"; var IS_BACK = "_isBack"; var NAV_DEPTH = "_navDepth"; var CLEARING_HISTORY = "_clearingHistory"; var FRAMEID = "_frameId"; var navDepth = -1; var activityInitialized = false; var animationFixed; function ensureAnimationFixed() { if (!animationFixed) { animationFixed = android.os.Build.VERSION.SDK_INT >= 19 // android.os.Build.VERSION.KITKAT but we don't have definition for it ? 1 : -1; } } var FragmentClass; function ensureFragmentClass() { if (FragmentClass) { return; } FragmentClass = (android.app.Fragment).extend({ onCreate: function (savedInstanceState: android.os.Bundle) { trace.write(`${this.getTag()}.onCreate(${savedInstanceState})`, trace.categories.NativeLifecycle); this.super.onCreate(savedInstanceState); this.super.setHasOptionsMenu(true); // 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) { let frameId = (this).getArguments().getInt(FRAMEID); let frame = getFrameById(frameId); if (frame) { this.frame = frame; } else { throw new Error(`Cannot find Frame for ${this}`); } findPageForFragment(this, this.frame); } }, onCreateView: function (inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { trace.write(`${this.getTag()}.onCreateView(inflater, container, ${savedInstanceState})`, trace.categories.NativeLifecycle); var entry = this.entry; var page = entry.resolvedPage; if (savedInstanceState && savedInstanceState.getBoolean(HIDDEN, false)) { this.super.getFragmentManager().beginTransaction().hide(this).commit(); page._onAttached(this.getActivity()); } else { onFragmentShown(this); } return page._nativeView; }, onHiddenChanged: function (hidden: boolean) { trace.write(`${this.getTag()}.onHiddenChanged(${hidden})`, trace.categories.NativeLifecycle); this.super.onHiddenChanged(hidden); if (hidden) { onFragmentHidden(this); } else { onFragmentShown(this); } }, onSaveInstanceState: function (outState: android.os.Bundle) { trace.write(`${this.getTag()}.onSaveInstanceState(${outState})`, trace.categories.NativeLifecycle); this.super.onSaveInstanceState(outState); if (this.isHidden()) { outState.putBoolean(HIDDEN, true); } }, onDestroyView: function () { trace.write(`${this.getTag()}.onDestroyView()`, trace.categories.NativeLifecycle); this.super.onDestroyView(); onFragmentHidden(this); // When Fragment is destroyed we detach page even if cachePagesOnNavigate is true. let entry: definition.BackstackEntry = this.entry; let page = entry.resolvedPage; if (page._context) { page._onDetached(true); } }, onDestroy: function () { trace.write(`${this.getTag()}.onDestroy()`, trace.categories.NativeLifecycle); this.super.onDestroy(); utils.GC(); }, onCreateAnimator: function (transit: number, enter: boolean, nextAnim: number): android.animation.Animator { var animator = transitionModule._onFragmentCreateAnimator(this, nextAnim); if (!animator) { animator = this.super.onCreateAnimator(transit, enter, nextAnim); } trace.write(`${this.getTag()}.onCreateAnimator(${transit}, ${enter}, ${nextAnim}): ${animator}`, trace.categories.NativeLifecycle); return animator; } }); } function onFragmentShown(fragment) { trace.write(`SHOWN ${fragment.getTag()}`, trace.categories.NativeLifecycle); if (fragment[CLEARING_HISTORY]) { trace.write(`${fragment.getTag()} has been shown, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle); return null; } // TODO: consider putting entry and page in queue so we can safely extract them here. Pass the index of current navigation and extract it from here. // After extracting navigation info - remove this index from navigation stack. var frame = fragment.frame; var entry: definition.BackstackEntry = fragment.entry; var page: pages.Page = entry.resolvedPage; let currentNavigationContext; let navigationQueue = frame._navigationQueue; for (let i = 0; i < navigationQueue.length; i++) { if (navigationQueue[i].entry === entry) { currentNavigationContext = navigationQueue[i]; break; } } var isBack = currentNavigationContext ? currentNavigationContext.isBackNavigation : false; frame._addView(page); // onFragmentShown is called before NativeActivity.start where we call frame.onLoaded // We need to call frame.onLoaded() here so that the call to frame._addView(page) will emit the page.loaded event // before the page.navigatedTo event making the two platforms identical. if (!frame.isLoaded) { frame._currentEntry = entry; frame.onLoaded(); } // Handle page transitions. transitionModule._onFragmentShown(fragment, isBack); } function onFragmentHidden(fragment) { trace.write(`HIDDEN ${fragment.getTag()}`, trace.categories.NativeLifecycle); if (fragment[CLEARING_HISTORY]) { trace.write(`${fragment.getTag()} has been hidden, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle); return null; } var isBack = fragment.entry[IS_BACK]; fragment.entry[IS_BACK] = undefined; // Handle page transitions. transitionModule._onFragmentHidden(fragment, isBack); } export class Frame extends frameCommon.Frame { private _android: AndroidFrame; private _delayedNavigationEntry: definition.BackstackEntry; private _containerViewId: number = -1; private _listener: android.view.View.OnAttachStateChangeListener; constructor() { super(); this._android = new AndroidFrame(this); this._listener = new android.view.View.OnAttachStateChangeListener({ onViewAttachedToWindow: this.onNativeViewAttachedToWindow.bind(this), onViewDetachedFromWindow: this.onNativeViewDetachedToWindow.bind(this) }); } public static get defaultAnimatedNavigation(): boolean { return frameCommon.Frame.defaultAnimatedNavigation; } public static set defaultAnimatedNavigation(value: boolean) { frameCommon.Frame.defaultAnimatedNavigation = value; } public static get defaultTransition(): definition.NavigationTransition { return frameCommon.Frame.defaultTransition; } public static set defaultTransition(value: definition.NavigationTransition) { frameCommon.Frame.defaultTransition = value; } get containerViewId(): number { return this._containerViewId; } get android(): AndroidFrame { return this._android; } get _nativeView(): any { return this._android.rootViewGroup; } public _navigateCore(backstackEntry: definition.BackstackEntry) { trace.write(`${this}._navigateCore(page: ${backstackEntry.resolvedPage}, backstackVisible: ${this._isEntryBackstackVisible(backstackEntry)}, clearHistory: ${backstackEntry.entry.clearHistory}), navDepth: ${navDepth}`, trace.categories.Navigation); let activity = this._android.activity; if (!activity) { // We do not have an Activity yet 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 let currentActivity = this._android.currentActivity; if (currentActivity) { startActivity(currentActivity, this._android.frameId); } this._delayedNavigationEntry = backstackEntry; return; } let manager = activity.getFragmentManager(); let isFirstNavigation = types.isNullOrUndefined(this._currentEntry); // Clear history if (backstackEntry.entry.clearHistory) { let backStackEntryCount = manager.getBackStackEntryCount(); let i = backStackEntryCount - 1; let fragment: android.app.Fragment; while (i >= 0) { fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); trace.write(`${fragment.getTag()}[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle); fragment[CLEARING_HISTORY] = true; } // Remember that the current fragment has never been added to the backStack, so mark it as well. if (this.currentPage) { fragment = manager.findFragmentByTag(this.currentPage[TAG]); if (fragment) { fragment[CLEARING_HISTORY] = true; trace.write(`${fragment.getTag()}[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle); } } if (backStackEntryCount) { let firstEntryName = manager.getBackStackEntryAt(0).getName(); trace.write(`manager.popBackStack(${firstEntryName}, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);`, trace.categories.NativeLifecycle); manager.popBackStack(firstEntryName, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } this._currentEntry = null; navDepth = -1; } navDepth++; let fragmentTransaction = manager.beginTransaction(); var currentFragmentTag: string; var currentFragment: android.app.Fragment; if (this.currentPage) { currentFragmentTag = this.currentPage[TAG]; currentFragment = manager.findFragmentByTag(currentFragmentTag); } var newFragmentTag = "fragment" + navDepth; ensureFragmentClass(); let newFragment = new FragmentClass(); let args = new android.os.Bundle(); args.putInt(FRAMEID, this._android.frameId); newFragment.setArguments(args); var animated = this._getIsAnimatedNavigation(backstackEntry.entry); var navigationTransition = this._getNavigationTransition(backstackEntry.entry); if (currentFragment) { // There might be transitions left over from previous forward navigations from the current page. transitionModule._clearForwardTransitions(currentFragment); } if (animated && navigationTransition) { transitionModule._setAndroidFragmentTransitions(navigationTransition, currentFragment, newFragment, fragmentTransaction); } newFragment.frame = this; newFragment.entry = backstackEntry; backstackEntry[BACKSTACK_TAG] = newFragmentTag; backstackEntry[NAV_DEPTH] = navDepth; // remember the fragment tag at page level so that we can retrieve the fragment associated with a Page instance backstackEntry.resolvedPage[TAG] = newFragmentTag; if (isFirstNavigation) { fragmentTransaction.add(this.containerViewId, newFragment, newFragmentTag); trace.write(`fragmentTransaction.add(${newFragmentTag});`, trace.categories.NativeLifecycle); } else { if (this.android.cachePagesOnNavigate && !backstackEntry.entry.clearHistory) { if (currentFragment) { fragmentTransaction.hide(currentFragment); trace.write(`fragmentTransaction.hide(${currentFragmentTag});`, trace.categories.NativeLifecycle); } else { trace.write(`Could not find ${currentFragmentTag} to hide.`, trace.categories.NativeLifecycle); } fragmentTransaction.add(this.containerViewId, newFragment, newFragmentTag); trace.write(`fragmentTransaction.add(${newFragmentTag});`, trace.categories.NativeLifecycle); } else { fragmentTransaction.replace(this.containerViewId, newFragment, newFragmentTag); trace.write(`fragmentTransaction.replace(${newFragmentTag});`, trace.categories.NativeLifecycle); } // Add to backStack if needed. if (this.backStack.length > 0 && this._currentEntry) { // We add each entry in the backstack to avoid the "Stack corrupted" mismatch let backstackTag = this._currentEntry[BACKSTACK_TAG]; fragmentTransaction.addToBackStack(backstackTag); trace.write(`fragmentTransaction.addToBackStack(${backstackTag});`, trace.categories.NativeLifecycle); } } if (!isFirstNavigation) { // This bug is fixed on API19+ ensureAnimationFixed(); if (this.android.cachePagesOnNavigate && animationFixed < 0) { // Apparently, there is an Android bug with when hiding fragments with animation. // https://code.google.com/p/android/issues/detail?id=32405 // When bug is fixed use animated variable. fragmentTransaction.setTransition(android.app.FragmentTransaction.TRANSIT_NONE); } else { var transit = animated ? android.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN : android.app.FragmentTransaction.TRANSIT_NONE; fragmentTransaction.setTransition(transit); } } fragmentTransaction.commit(); trace.write(`fragmentTransaction.commit();`, trace.categories.NativeLifecycle); } public _goBackCore(backstackEntry: definition.BackstackEntry) { navDepth = backstackEntry[NAV_DEPTH]; if (this._currentEntry) { // We need this information inside onFragmentHidden this._currentEntry[IS_BACK] = true; } trace.write(`${this}._goBackCore(pageId: ${backstackEntry.resolvedPage.id}, backstackVisible: ${this._isEntryBackstackVisible(backstackEntry)}, clearHistory: ${backstackEntry.entry.clearHistory}), navDepth: ${navDepth}`, trace.categories.Navigation); var manager = this._android.activity.getFragmentManager(); if (manager.getBackStackEntryCount() > 0) { // 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[BACKSTACK_TAG], android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } } public _createUI() { let root = new org.nativescript.widgets.ContentLayout(this._context); if (this._containerViewId < 0) { this._containerViewId = android.view.View.generateViewId(); } this._android.rootViewGroup = root; this._android.rootViewGroup.setId(this._containerViewId); this._android.rootViewGroup.addOnAttachStateChangeListener(this._listener); } private onNativeViewAttachedToWindow(view: android.view.View): void { if (this._delayedNavigationEntry) { this._navigateCore(this._delayedNavigationEntry); this._delayedNavigationEntry = undefined; } } private onNativeViewDetachedToWindow(view: android.view.View): void { // unused for the moment. } public _popFromFrameStack() { if (!this._isInFrameStack) { return; } super._popFromFrameStack(); if (this._android.hasOwnActivity) { this._android.activity.finish(); } } public _clearAndroidReference() { this._android.rootViewGroup.removeOnAttachStateChangeListener(this._listener); // we should keep the reference to underlying native object, since frame can contain many pages. this._android.rootViewGroup = null; } public _printNativeBackStack() { if (!this._android.activity) { return; } var manager = this._android.activity.getFragmentManager(); var length = manager.getBackStackEntryCount(); var i = length - 1; console.log("---------------------------"); console.log("Fragment Manager Back Stack (" + length + ")"); while (i >= 0) { var fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); console.log("[ " + fragment.getTag() + " ]"); } } public _printFrameBackStack() { var length = this.backStack.length; var i = length - 1; console.log("---------------------------"); console.log("Frame Back Stack (" + length + ")"); while (i >= 0) { var backstackEntry = this.backStack[i--]; console.log("[ " + backstackEntry.resolvedPage.id + " ]"); } } public _getNavBarVisible(page: pages.Page): boolean { if (types.isDefined(page.actionBarHidden)) { return !page.actionBarHidden; } if (this._android && types.isDefined(this._android.showActionBar)) { return this._android.showActionBar; } return true; } } var NativeActivity = { get rootView(): View { return this[ROOT_VIEW]; }, onCreate: function (savedInstanceState: android.os.Bundle) { trace.write(`NativeScriptActivity.onCreate(${savedInstanceState})`, trace.categories.NativeLifecycle); let app = application.android; let activity: android.app.Activity = this; let intent = activity.getIntent(); if (application.onLaunch) { application.onLaunch(intent); } let args: application.LaunchEventData = { eventName: application.launchEvent, object: app, android: intent }; application.notify(args); let frameId = -1; let rootView = args.root; let extras = intent.getExtras(); // We have extras when we call - new Frame().navigate(); // savedInstanceState is used when activity is recreated. if (extras) { frameId = extras.getInt(INTENT_EXTRA, -1); } else if (savedInstanceState) { 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); } else if (!rootView) { navParam = application.mainEntry; if (!navParam) { navParam = application.mainModule; } 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. var isRestart = !!savedInstanceState && activityInitialized; this.super.onCreate(isRestart ? savedInstanceState : null); this[ROOT_VIEW] = rootView; // Initialize native visual tree; rootView._onAttached(this); this.setContentView(rootView._nativeView, new org.nativescript.widgets.CommonLayoutParams()); // frameId is negative w if (frame) { frame.navigate(navParam); } activityInitialized = true; // TODO: If the above fails because we call fragmentManager.beginTransition().commit() before // we are added as content to activity - add if (rootview instanceof Frame) -> call navigate //this.frame._onActivityCreated(isRestart); }, onSaveInstanceState(outState: android.os.Bundle): void { this.super.onSaveInstanceState(outState); let view = this.rootView; if (view instanceof Frame) { outState.putInt(INTENT_EXTRA, (view).android.frameId); } }, onActivityResult: function (requestCode: number, resultCode: number, data: android.content.Intent) { this.super.onActivityResult(requestCode, resultCode, data); trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, trace.categories.NativeLifecycle); var result = application.android.onActivityResult; if (result) { result(requestCode, resultCode, data); } application.android.notify({ eventName: "activityResult", object: application.android, activity: this, requestCode: requestCode, resultCode: resultCode, intent: data }); }, onStart: function () { this.super.onStart(); trace.write("NativeScriptActivity.onStart();", trace.categories.NativeLifecycle); let rootView: View = this.rootView if (rootView && !rootView.isLoaded) { rootView.onLoaded(); } }, onStop: function () { this.super.onStop(); trace.write("NativeScriptActivity.onStop();", trace.categories.NativeLifecycle); let rootView: View = this.rootView if (rootView && rootView.isLoaded) { rootView.onUnloaded(); } }, onDestroy: function () { let rootView: View = this.rootView if (rootView && rootView._context) { rootView._onDetached(true); } this.super.onDestroy(); trace.write("NativeScriptActivity.onDestroy();", trace.categories.NativeLifecycle); }, onBackPressed: function () { trace.write("NativeScriptActivity.onBackPressed;", trace.categories.NativeLifecycle); var args = { eventName: "activityBackPressed", object: application.android, activity: this, cancel: false, }; application.android.notify(args); if (args.cancel) { return; } if (!frameCommon.goBack()) { this.super.onBackPressed(); } }, onLowMemory: function () { trace.write("NativeScriptActivity.onLowMemory()", trace.categories.NativeLifecycle); gc(); java.lang.System.gc(); this.super.onLowMemory(); application.notify({ eventName: application.lowMemoryEvent, object: this, android: this }); }, onTrimMemory: function (level: number) { trace.write(`NativeScriptActivity.onTrimMemory(${level})`, trace.categories.NativeLifecycle); gc(); java.lang.System.gc(); this.super.onTrimMemory(level); } }; var framesCounter = 0; var framesCache: Array> = new Array>(); class AndroidFrame extends Observable implements definition.AndroidFrame { public rootViewGroup: android.view.ViewGroup; public hasOwnActivity = false; public frameId; private _showActionBar = true; private _owner: Frame; private _cachePagesOnNavigate: boolean; 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 stack = frameCommon.stack(), length = stack.length, i = length - 1, frame: definition.Frame; for (i; i >= 0; i--) { frame = stack[i]; activity = frame.android.activity; if (activity) { return activity; } } return undefined; } public get owner(): Frame { return this._owner; } public get cachePagesOnNavigate(): boolean { return this._cachePagesOnNavigate; } public set cachePagesOnNavigate(value: boolean) { if (this._cachePagesOnNavigate !== value) { if (this._owner.backStack.length > 0) { throw new Error("Cannot set cachePagesOnNavigate if there are items in the back stack."); } this._cachePagesOnNavigate = value; } } 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; } } function findPageForFragment(fragment: android.app.Fragment, frame: Frame) { var fragmentTag = fragment.getTag(); var page: pages.Page; var entry: definition.BackstackEntry; trace.write(`Finding page for ${fragmentTag}.`, trace.categories.NativeLifecycle); if (fragmentTag === (pages).DIALOG_FRAGMENT_TAG) { trace.write(`No need to find page for dialog fragment.`, trace.categories.NativeLifecycle); return; } if (frame.currentPage && frame.currentPage[TAG] === fragmentTag) { page = frame.currentPage; entry = frame._currentEntry; trace.write(`Current page matches fragment ${fragmentTag}.`, trace.categories.NativeLifecycle); } else { var backStack = frame.backStack; for (var i = 0; i < backStack.length; i++) { if (backStack[i].resolvedPage[TAG] === fragmentTag) { entry = backStack[i]; break; } } if (entry) { page = entry.resolvedPage; trace.write(`Found ${page} for ${fragmentTag}`, trace.categories.NativeLifecycle); } } if (page) { (fragment).frame = frame; (fragment).entry = entry; page[TAG] = fragmentTag; } else { //throw new Error(`Could not find a page for ${fragmentTag}.`); } } function startActivity(activity: android.app.Activity, frameId: number) { var intent = new android.content.Intent(activity, (com).tns.NativeScriptActivity.class); 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; } export function getActivity(): Object { return NativeActivity; }