diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 92828b9623..5cf2d105a7 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -1,3 +1,4 @@ +import { embedded } from 'ui/embedding'; import { profile } from '../profiling'; import { View } from '../ui'; import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common'; @@ -10,6 +11,12 @@ declare namespace com { class NativeScriptApplication extends android.app.Application { static getInstance(): NativeScriptApplication; } + + namespace embedding { + class ApplicationHolder { + static getInstance(): android.app.Application; + } + } } } @@ -358,6 +365,10 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp nativeApp = com.tns.NativeScriptApplication.getInstance(); } + if (!nativeApp && embedded()) { + nativeApp = com.tns.embedding.ApplicationHolder.getInstance(); + } + // the getInstance might return null if com.tns.NativeScriptApplication exists but is not the starting app type if (!nativeApp) { // TODO: Should we handle the case when a custom application type is provided and the user has not explicitly initialized the application module? diff --git a/packages/core/ui/embedding/index.ts b/packages/core/ui/embedding/index.ts new file mode 100644 index 0000000000..dce30fbd71 --- /dev/null +++ b/packages/core/ui/embedding/index.ts @@ -0,0 +1,26 @@ +import { View } from '../../ui/core/view'; + +declare namespace org { + namespace nativescript { + class Bootstrap { + static isEmbeddedNativeScript: boolean; + } + } +} + +export function embedded(): boolean { + return org.nativescript.Bootstrap.isEmbeddedNativeScript; +} + +let view: View | undefined; + +export function setContentView(contentView: View | undefined): void { + view = contentView; +} + +export function getContentView(): View { + if (!view) { + throw new Error("{N} Core: Fragment content view not set or set to 'undefined'"); + } + return view; +} diff --git a/packages/core/ui/frame/activity.android.ts b/packages/core/ui/frame/activity.android.ts index a526453b9f..e4b60df878 100644 --- a/packages/core/ui/frame/activity.android.ts +++ b/packages/core/ui/frame/activity.android.ts @@ -1,62 +1,137 @@ import '../../globals'; -import { setActivityCallbacks, AndroidActivityCallbacks } from '.'; +import { setActivityCallbacks } from '.'; import { Application } from '../../application'; +import { embedded } from 'ui/embedding'; -/** - * NOTE: We cannot use NativeClass here because this is used in appComponents in webpack.config - * Whereby it bypasses the decorator transformation, hence pure es5 style written here - */ -const superProto = androidx.appcompat.app.AppCompatActivity.prototype; -(androidx.appcompat.app.AppCompatActivity).extend('com.tns.NativeScriptActivity', { - init() { - // init must at least be defined - }, - onCreate(savedInstanceState: android.os.Bundle): void { - Application.android.init(this.getApplication()); - - // Set isNativeScriptActivity in onCreate. - // The JS constructor might not be called because the activity is created from Android. - this.isNativeScriptActivity = true; - if (!this._callbacks) { - setActivityCallbacks(this); - } - - this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), superProto.onCreate); - }, - - onNewIntent(intent: android.content.Intent): void { - this._callbacks.onNewIntent(this, intent, superProto.setIntent, superProto.onNewIntent); - }, - - onSaveInstanceState(outState: android.os.Bundle): void { - this._callbacks.onSaveInstanceState(this, outState, superProto.onSaveInstanceState); - }, - - onStart(): void { - this._callbacks.onStart(this, superProto.onStart); - }, - - onStop(): void { - this._callbacks.onStop(this, superProto.onStop); - }, - - onDestroy(): void { - this._callbacks.onDestroy(this, superProto.onDestroy); - }, - - onPostResume(): void { - this._callbacks.onPostResume(this, superProto.onPostResume); - }, - - onBackPressed(): void { - this._callbacks.onBackPressed(this, superProto.onBackPressed); - }, - - onRequestPermissionsResult(requestCode: number, permissions: Array, grantResults: Array): void { - this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/); - }, - - onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void { - this._callbacks.onActivityResult(this, requestCode, resultCode, data, superProto.onActivityResult); - }, -}); +const isEmbedded = embedded(); + +const EMPTY_FN = () => {}; +declare const com: any; + +if (!isEmbedded) { + /** + * NOTE: We cannot use NativeClass here because this is used in appComponents in webpack.config + * Whereby it bypasses the decorator transformation, hence pure es5 style written here + */ + const superProto = androidx.appcompat.app.AppCompatActivity.prototype; + (androidx.appcompat.app.AppCompatActivity).extend('com.tns.NativeScriptActivity', { + init() { + // init must at least be defined + }, + onCreate(savedInstanceState: android.os.Bundle): void { + Application.android.init(this.getApplication()); + + // Set isNativeScriptActivity in onCreate. + // The JS constructor might not be called because the activity is created from Android. + this.isNativeScriptActivity = true; + if (!this._callbacks) { + setActivityCallbacks(this); + } + + this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), superProto.onCreate); + }, + + onNewIntent(intent: android.content.Intent): void { + this._callbacks.onNewIntent(this, intent, superProto.setIntent, superProto.onNewIntent); + }, + + onSaveInstanceState(outState: android.os.Bundle): void { + this._callbacks.onSaveInstanceState(this, outState, superProto.onSaveInstanceState); + }, + + onStart(): void { + this._callbacks.onStart(this, superProto.onStart); + }, + + onStop(): void { + this._callbacks.onStop(this, superProto.onStop); + }, + + onDestroy(): void { + this._callbacks.onDestroy(this, superProto.onDestroy); + }, + + onPostResume(): void { + this._callbacks.onPostResume(this, superProto.onPostResume); + }, + + onBackPressed(): void { + this._callbacks.onBackPressed(this, superProto.onBackPressed); + }, + + onRequestPermissionsResult(requestCode: number, permissions: Array, grantResults: Array): void { + this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/); + }, + + onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void { + this._callbacks.onActivityResult(this, requestCode, resultCode, data, superProto.onActivityResult); + }, + }); +} else { + const Callbacks = com.tns.embedding.EmbeddableActivityCallbacks.extend({ + init() { + // init must at least be defined + }, + onCreate(savedInstanceState: android.os.Bundle): void { + const activity = this.getActivity(); + + Application.android.init(activity.getApplication()); + + // Set isNativeScriptActivity in onCreate. + // The JS constructor might not be called because the activity is created from Android. + activity.isNativeScriptActivity = true; + if (!activity._callbacks) { + setActivityCallbacks(activity); + } + + activity._callbacks.onCreate(activity, savedInstanceState, activity.getIntent(), EMPTY_FN); + }, + + onNewIntent(intent: android.content.Intent): void { + const activity = this.getActivity(); + activity._callbacks.onNewIntent(activity, intent, EMPTY_FN, EMPTY_FN); + }, + + onSaveInstanceState(outState: android.os.Bundle): void { + const activity = this.getActivity(); + activity._callbacks.onSaveInstanceState(activity, outState, EMPTY_FN); + }, + + onStart(): void { + const activity = this.getActivity(); + activity._callbacks.onStart(activity, EMPTY_FN); + }, + + onStop(): void { + const activity = this.getActivity(); + activity._callbacks.onStop(activity, EMPTY_FN); + }, + + onDestroy(): void { + const activity = this.getActivity(); + activity._callbacks.onDestroy(activity, EMPTY_FN); + }, + + onPostResume(): void { + const activity = this.getActivity(); + activity._callbacks.onPostResume(activity, EMPTY_FN); + }, + + onBackPressed(): void { + const activity = this.getActivity(); + activity._callbacks.onBackPressed(activity, EMPTY_FN); + }, + + onRequestPermissionsResult(requestCode: number, permissions: Array, grantResults: Array): void { + const activity = this.getActivity(); + activity._callbacks.onRequestPermissionsResult(activity, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/); + }, + + onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void { + const activity = this.getActivity(); + activity._callbacks.onActivityResult(activity, requestCode, resultCode, data, EMPTY_FN); + }, + }); + + com.tns.embedding.CallbacksStore.setActivityCallbacks(new Callbacks()); +} diff --git a/packages/core/ui/frame/callbacks/activity-callbacks.ts b/packages/core/ui/frame/callbacks/activity-callbacks.ts new file mode 100644 index 0000000000..5a9b692496 --- /dev/null +++ b/packages/core/ui/frame/callbacks/activity-callbacks.ts @@ -0,0 +1,304 @@ +import { AndroidActivityCallbacks, Frame } from './..'; + +import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, Application } from '../../../application'; + +import { Trace } from '../../../trace'; +import { View } from '../../core/view'; +import { _stack, FrameBase } from './../frame-common'; + +import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions } from './../fragment.transitions'; + +import { profile } from '../../../profiling'; +import { embedded, setContentView as embeddedSetContentView } from '../../../ui/embedding'; + +const activityRootViewsMap = new Map>(); +const INTENT_EXTRA = 'com.tns.activity'; +const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId'; + +export let moduleLoaded: boolean; + +export class ActivityCallbacksImplementation implements AndroidActivityCallbacks { + private _rootView: View; + + public getRootView(): View { + return this._rootView; + } + + @profile + public onCreate(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, intentOrSuperFunc: android.content.Intent | Function, superFunc?: Function): void { + if (Trace.isEnabled()) { + Trace.write(`Activity.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle); + } + + const intent: android.content.Intent = superFunc ? intentOrSuperFunc : undefined; + + if (!superFunc) { + console.log('AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, superFunc: Function) ' + 'is deprecated. Use AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function) instead.'); + superFunc = intentOrSuperFunc; + } + + // 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 moduleLoaded 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. + const isRestart = !!savedInstanceState && moduleLoaded; + superFunc.call(activity, isRestart ? savedInstanceState : null); + + // Try to get the rootViewId form the saved state in case the activity + // was destroyed and we are now recreating it. + if (savedInstanceState) { + const rootViewId = savedInstanceState.getInt(ROOT_VIEW_ID_EXTRA, -1); + if (rootViewId !== -1 && activityRootViewsMap.has(rootViewId)) { + this._rootView = activityRootViewsMap.get(rootViewId)?.get(); + } + } + + if (intent && intent.getAction()) { + Application.android.notify({ + eventName: Application.AndroidApplication.activityNewIntentEvent, + object: Application.android, + activity, + intent, + }); + } + + this.setActivityContent(activity, savedInstanceState, true); + moduleLoaded = true; + } + + @profile + public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void { + superFunc.call(activity, outState); + const rootView = this._rootView; + if (rootView instanceof Frame) { + outState.putInt(INTENT_EXTRA, rootView.android.frameId); + rootView._saveFragmentsState(); + } + + if (rootView) { + outState.putInt(ROOT_VIEW_ID_EXTRA, rootView._domId); + } + } + + @profile + public onNewIntent(activity: androidx.appcompat.app.AppCompatActivity, intent: android.content.Intent, superSetIntentFunc: Function, superFunc: Function): void { + superFunc.call(activity, intent); + superSetIntentFunc.call(activity, intent); + + Application.android.notify({ + eventName: Application.AndroidApplication.activityNewIntentEvent, + object: Application.android, + activity, + intent, + }); + } + + @profile + public onStart(activity: any, superFunc: Function): void { + superFunc.call(activity); + + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onStart();', Trace.categories.NativeLifecycle); + } + + const rootView = this._rootView; + if (rootView && !rootView.isLoaded && !embedded()) { + rootView.callLoaded(); + } + } + + @profile + public onStop(activity: any, superFunc: Function): void { + superFunc.call(activity); + + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle); + } + + const rootView = this._rootView; + if (rootView && rootView.isLoaded && !embedded()) { + rootView.callUnloaded(); + } + } + + @profile + public onPostResume(activity: any, superFunc: Function): void { + superFunc.call(activity); + + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onPostResume();', Trace.categories.NativeLifecycle); + } + + // NOTE: activity.onPostResume() is called when activity resume is complete and we can + // safely raise the application resume event; + // onActivityResumed(...) lifecycle callback registered in application is called too early + // and raising the application resume event there causes issues like + // https://github.com/NativeScript/NativeScript/issues/6708 + if ((activity).isNativeScriptActivity) { + Application.setSuspended(false, { + // todo: deprecate in favor of using event.activity instead. + android: activity, + activity, + }); + } + } + + @profile + public onDestroy(activity: any, superFunc: Function): void { + try { + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle); + } + + const rootView = this._rootView; + if (rootView) { + rootView._tearDownUI(true); + } + + // this may happen when the user changes the system theme + // In such case, isFinishing() is false (and isChangingConfigurations is true), and the app will start again (onCreate) with a savedInstanceState + // as a result, launchEvent will never be called + // possible alternative: always fire launchEvent and exitEvent, but pass extra flags to make it clear what kind of launch/destroy is happening + if (activity.isFinishing()) { + const exitArgs = { + eventName: Application.exitEvent, + object: Application.android, + android: activity, + }; + Application.notify(exitArgs); + } + } finally { + superFunc.call(activity); + } + } + + @profile + public onBackPressed(activity: any, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onBackPressed;', Trace.categories.NativeLifecycle); + } + + const args = { + eventName: 'activityBackPressed', + object: Application, + android: Application.android, + activity: activity, + cancel: false, + }; + Application.android.notify(args); + if (args.cancel) { + return; + } + + const view = this._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 ? !FrameBase.goBack() : true; + } + + if (callSuper) { + superFunc.call(activity); + } + } + + @profile + public onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array, grantResults: Array, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write('NativeScriptActivity.onRequestPermissionsResult;', Trace.categories.NativeLifecycle); + } + + Application.android.notify({ + eventName: 'activityRequestPermissions', + object: Application, + android: 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 (Trace.isEnabled()) { + Trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, Trace.categories.NativeLifecycle); + } + + Application.android.notify({ + eventName: 'activityResult', + object: Application, + android: Application.android, + activity: activity, + requestCode: requestCode, + resultCode: resultCode, + intent: data, + }); + } + + public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity): void { + if (this._rootView) { + const manager = this._rootView._getFragmentManager(); + manager.executePendingTransactions(); + + this._rootView._onRootViewReset(); + } + // Delete previously cached root view in order to recreate it. + this._rootView = null; + this.setActivityContent(activity, null, false); + this._rootView.callLoaded(); + } + + // Paths that go trough this method: + // 1. Application initial start - there is no rootView in callbacks. + // 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate. + // 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired + // 4. resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired + private setActivityContent(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean): void { + let rootView = this._rootView; + + if (Trace.isEnabled()) { + Trace.write(`Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, Trace.categories.NativeLifecycle); + } + + const intent = activity.getIntent(); + rootView = Application.createRootView(rootView, fireLaunchEvent, { + // todo: deprecate in favor of args.intent? + android: intent, + intent, + savedInstanceState, + }); + + if (!rootView) { + // no root view created + return; + } + + activityRootViewsMap.set(rootView._domId, new WeakRef(rootView)); + + // setup view as styleScopeHost + rootView._setupAsRootView(activity); + + if (embedded()) { + embeddedSetContentView(rootView); + } else { + activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams()); + } + + this._rootView = rootView; + + // sets root classes once rootView is ready... + Application.initRootView(rootView); + } +} diff --git a/packages/core/ui/frame/callbacks/fragment-callbacks.ts b/packages/core/ui/frame/callbacks/fragment-callbacks.ts new file mode 100644 index 0000000000..2f8f36df9f --- /dev/null +++ b/packages/core/ui/frame/callbacks/fragment-callbacks.ts @@ -0,0 +1,311 @@ +import { profile } from '../../../profiling'; +import { AndroidFragmentCallbacks, BackstackEntry, Frame } from '..'; +import { Trace } from '../../../trace'; +import { Application } from '../../../application'; +import { Color } from '../../../color'; +import { getFrameByNumberId } from '../index.android'; +import { _updateTransitions } from '../fragment.transitions'; +import { Page } from 'ui/page'; + +const FRAMEID = '_frameId'; +const CALLBACKS = '_callbacks'; + +export class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { + public frame: Frame; + public entry: BackstackEntry; + private backgroundBitmap: android.graphics.Bitmap = null; + + @profile + public onHiddenChanged(fragment: androidx.fragment.app.Fragment, hidden: boolean, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onHiddenChanged(${hidden})`, Trace.categories.NativeLifecycle); + } + superFunc.call(fragment, hidden); + } + + @profile + public onCreateAnimator(fragment: androidx.fragment.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { + let animator = null; + const entry = this.entry; + + // Return enterAnimator only when new (no current entry) nested transition. + if (enter && entry.isNestedDefaultTransition) { + animator = entry.enterAnimator; + entry.isNestedDefaultTransition = false; + } + + return animator || superFunc.call(fragment, transit, enter, nextAnim); + } + + @profile + public onCreate(fragment: androidx.fragment.app.Fragment, savedInstanceState: android.os.Bundle, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onCreate(${savedInstanceState})`, Trace.categories.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 args = fragment.getArguments(); + const frameId = args.getInt(FRAMEID); + const frame = getFrameByNumberId(frameId); + if (!frame) { + throw new Error(`Cannot find Frame for ${fragment}`); + } + + findPageForFragment(fragment, frame); + } + } + + @profile + public onCreateView(fragment: androidx.fragment.app.Fragment, inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle, superFunc: Function): android.view.View { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onCreateView(inflater, container, ${savedInstanceState})`, Trace.categories.NativeLifecycle); + } + + const entry = this.entry; + if (!entry) { + Trace.error(`${fragment}.onCreateView: entry is null or undefined`); + + return null; + } + + const page = entry.resolvedPage; + if (!page) { + Trace.error(`${fragment}.onCreateView: entry has no resolvedPage`); + + return null; + } + + const frame = this.frame; + if (!frame) { + Trace.error(`${fragment}.onCreateView: this.frame is null or undefined`); + + return null; + } + + frame._resolvedPage = page; + + if (page.parent === frame) { + frame._inheritStyles(page); + + // If we are navigating to a page that was destroyed + // reinitialize its UI. + if (!page._context) { + const context = (container && container.getContext()) || (inflater && inflater.getContext()); + page._setupUI(context); + } + + if (frame.isLoaded && !page.isLoaded) { + page.callLoaded(); + } + } else { + if (!page.parent) { + if (!frame._styleScope) { + // Make sure page will have styleScope even if parents don't. + page._updateStyleScope(); + } + + frame._addView(page); + } else { + throw new Error('Page is already shown on another frame.'); + } + } + + const savedState = entry.viewSavedState; + if (savedState) { + (page.nativeViewProtected).restoreHierarchyState(savedState); + entry.viewSavedState = null; + } + + // fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'. + // on app resume in nested frame scenarios with support library version greater than 26.0.0 + // HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent + // right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a + // parent while its supposed parent believes it properly removed its children; in order to "force" the child to + // lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass) + const nativeView = page.nativeViewProtected; + if (nativeView != null) { + const parentView = nativeView.getParent(); + if (parentView instanceof android.view.ViewGroup) { + if (parentView.getChildCount() === 0) { + parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams()); + } + + parentView.removeAllViews(); + } + } + + return page.nativeViewProtected; + } + + @profile + public onSaveInstanceState(fragment: androidx.fragment.app.Fragment, outState: android.os.Bundle, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onSaveInstanceState(${outState})`, Trace.categories.NativeLifecycle); + } + superFunc.call(fragment, outState); + } + + @profile + public onDestroyView(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { + try { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onDestroyView()`, Trace.categories.NativeLifecycle); + } + + const hasRemovingParent = fragment.getRemovingParentFragment(); + + if (hasRemovingParent) { + const nativeFrameView = this.frame.nativeViewProtected; + if (nativeFrameView) { + const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(Application.android.context.getResources(), this.backgroundBitmap); + this.frame._originalBackground = this.frame.backgroundColor || new Color('White'); + nativeFrameView.setBackgroundDrawable(bitmapDrawable); + this.backgroundBitmap = null; + } + } + } finally { + superFunc.call(fragment); + } + } + + @profile + public onDestroy(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { + if (Trace.isEnabled()) { + Trace.write(`${fragment}.onDestroy()`, Trace.categories.NativeLifecycle); + } + + superFunc.call(fragment); + + const entry = this.entry; + if (!entry) { + Trace.error(`${fragment}.onDestroy: entry is null or undefined`); + + return null; + } + + // [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629 + // retaining reference to a destroyed fragment here somehow causes a cryptic + // "IllegalStateException: Failure saving state: active fragment has cleared index: -1" + // in a specific mixed parent / nested frame navigation scenario + entry.fragment = null; + + const page = entry.resolvedPage; + if (!page) { + // todo: check why this happens when using shared element transition!!! + // commented out the Trace.error to prevent a crash (the app will still work interestingly) + console.log(`${fragment}.onDestroy: entry has no resolvedPage`); + // Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`); + + return null; + } + } + + @profile + public onPause(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { + try { + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TODO: Consider removing it when update to androidx.fragment:1.2.0 + const hasRemovingParent = fragment.getRemovingParentFragment(); + + if (hasRemovingParent) { + this.backgroundBitmap = this.loadBitmapFromView(this.frame.nativeViewProtected); + } + } finally { + superFunc.call(fragment); + } + } + + @profile + public onResume(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { + const frame = this.entry.resolvedPage.frame; + // 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 + // so we manually set the entry + // also, to be compatible with fragments 1.2.x we need this setTimeout as animations haven't run on onResume yet + const weakRef = new WeakRef(this); + setTimeout(() => { + const owner = weakRef.get(); + if (!owner) { + return; + } + if (frame._executingContext && !(owner.entry).isAnimationRunning) { + frame.setCurrent(owner.entry, frame._executingContext.navigationType); + } + }, 0); + + superFunc.call(fragment); + } + + @profile + public onStop(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { + superFunc.call(fragment); + } + + @profile + public toStringOverride(fragment: androidx.fragment.app.Fragment, superFunc: Function): string { + const entry = this.entry; + if (entry) { + return `${entry.fragmentTag}<${entry.resolvedPage}>`; + } else { + return 'NO ENTRY, ' + superFunc.call(fragment); + } + } + + private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { + // Don't try to create bitmaps with no dimensions as this causes a crash + // This might happen when showing and closing dialogs fast. + if (!(view && view.getWidth() > 0 && view.getHeight() > 0)) { + return undefined; + } + + // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled + // const width = view.getWidth(); + // const height = view.getHeight(); + // const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); + // const canvas = new android.graphics.Canvas(bitmap); + // view.layout(0, 0, width, height); + // view.draw(canvas); + + // view.setDrawingCacheEnabled(true); + // const drawCache = view.getDrawingCache(); + // const bitmap = android.graphics.Bitmap.createBitmap(drawCache); + // view.setDrawingCacheEnabled(false); + return org.nativescript.widgets.Utils.getBitmapFromView(view); + } +} + +function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Frame) { + const fragmentTag = fragment.getTag(); + if (Trace.isEnabled()) { + Trace.write(`Finding page for ${fragmentTag}.`, Trace.categories.NativeLifecycle); + } + + let entry: BackstackEntry; + const current = frame._currentEntry; + const executingContext = frame._executingContext; + if (current && current.fragmentTag === fragmentTag) { + entry = current; + } else if (executingContext && executingContext.entry && executingContext.entry.fragmentTag === fragmentTag) { + entry = executingContext.entry; + } + + let page: Page; + if (entry) { + entry.recreated = true; + page = entry.resolvedPage; + } + + if (page) { + const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS]; + callbacks.frame = frame; + callbacks.entry = entry; + entry.fragment = fragment; + _updateTransitions(entry); + } else { + throw new Error(`Could not find a page for ${fragmentTag}.`); + } +} diff --git a/packages/core/ui/frame/fragment.android.ts b/packages/core/ui/frame/fragment.android.ts index 8ce9084137..17fd52fffd 100644 --- a/packages/core/ui/frame/fragment.android.ts +++ b/packages/core/ui/frame/fragment.android.ts @@ -1,4 +1,7 @@ -import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from '.'; +import { embedded, getContentView } from '../../ui/embedding'; +import { setFragmentCallbacks } from '.'; + +declare const com: any; const superProto = org.nativescript.widgets.FragmentBase.prototype; const FragmentClass = (org.nativescript.widgets.FragmentBase).extend('com.tns.FragmentClass', { @@ -33,9 +36,7 @@ const FragmentClass = (org.nativescript.widgets.FragmentBase).extend('com.t }, onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle) { - const result = this._callbacks.onCreateView(this, inflater, container, savedInstanceState, superProto.onCreateView); - - return result; + return this._callbacks.onCreateView(this, inflater, container, savedInstanceState, superProto.onCreateView); }, onSaveInstanceState(outState: android.os.Bundle) { @@ -61,4 +62,49 @@ const FragmentClass = (org.nativescript.widgets.FragmentBase).extend('com.t }, }); +export let fragmentClass: any; + +export function ensureFragmentClass() { + if (fragmentClass) { + return; + } + + // this require will apply the FragmentClass implementation + require('./fragment'); + + if (!fragmentClass) { + throw new Error('Failed to initialize the extended androidx.fragment.app.Fragment class'); + } +} + +export function setFragmentClass(clazz: any) { + if (fragmentClass) { + throw new Error('Fragment class already initialized'); + } + + if (embedded()) { + attachEmbeddableFragmentCallbacks(); + } + + fragmentClass = clazz; +} + +function attachEmbeddableFragmentCallbacks() { + const Callbacks = com.tns.embedding.EmbeddableFragmentCallbacks.extend({ + init() { + // init must at least be defined + }, + onCreateView() { + return getContentView().nativeViewProtected; + }, + onResume() { + getContentView().callLoaded(); + }, + onPause() { + getContentView().callUnloaded(); + }, + }); + com.tns.embedding.CallbacksStore.setFragmentCallbacks(new Callbacks()); +} + setFragmentClass(FragmentClass); diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 91f4d0b73a..7870715e56 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -1,12 +1,11 @@ // Definitions. -import { AndroidActivityCallbacks, AndroidFragmentCallbacks, AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationTransition } from '.'; +import { AndroidActivityCallbacks, AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationTransition } from '.'; import { Page } from '../page'; import { TransitionState } from './frame-common'; // Types. -import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, Application } from '../../application'; +import { Application } from '../../application'; -import { Color } from '../../color'; import { Observable } from '../../data/observable'; import { Trace } from '../../trace'; import { View } from '../core/view'; @@ -17,21 +16,23 @@ import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, import { profile } from '../../profiling'; import { android as androidUtils } from '../../utils/native-helper'; import type { ExpandedEntry } from './fragment.transitions.android'; +import { ensureFragmentClass, fragmentClass } from './fragment.android'; +import { FragmentCallbacksImplementation } from './callbacks/fragment-callbacks'; +import { ActivityCallbacksImplementation } from './callbacks/activity-callbacks'; export * from './frame-common'; +export { setFragmentClass } from './fragment.android'; const INTENT_EXTRA = 'com.tns.activity'; -const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId'; const FRAMEID = '_frameId'; const CALLBACKS = '_callbacks'; const ownerSymbol = Symbol('_owner'); -const activityRootViewsMap = new Map>(); let navDepth = -1; let fragmentId = -1; -export let moduleLoaded: boolean; +export { moduleLoaded } from './callbacks/activity-callbacks'; export let attachStateChangeListener: android.view.View.OnAttachStateChangeListener; @@ -756,38 +757,6 @@ class AndroidFrame extends Observable implements AndroidFrameDefinition { } } -function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Frame) { - const fragmentTag = fragment.getTag(); - if (Trace.isEnabled()) { - Trace.write(`Finding page for ${fragmentTag}.`, Trace.categories.NativeLifecycle); - } - - let entry: BackstackEntry; - const current = frame._currentEntry; - const executingContext = frame._executingContext; - if (current && current.fragmentTag === fragmentTag) { - entry = current; - } else if (executingContext && executingContext.entry && executingContext.entry.fragmentTag === fragmentTag) { - entry = executingContext.entry; - } - - let page: Page; - if (entry) { - entry.recreated = true; - page = entry.resolvedPage; - } - - if (page) { - const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS]; - callbacks.frame = frame; - callbacks.entry = entry; - entry.fragment = fragment; - _updateTransitions(entry); - } else { - throw new Error(`Could not find a page for ${fragmentTag}.`); - } -} - function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frameId: number) { // TODO: Implicitly, we will open the same activity type as the current one const intent = new android.content.Intent(activity, activity.getClass()); @@ -798,7 +767,7 @@ function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frame activity.startActivity(intent); } -function getFrameByNumberId(frameId: number): Frame { +export function getFrameByNumberId(frameId: number): Frame { // Find the frame for this activity. for (let i = 0; i < framesCache.length; i++) { const aliveFrame = framesCache[i].get(); @@ -810,578 +779,6 @@ function getFrameByNumberId(frameId: number): Frame { return null; } -function ensureFragmentClass() { - if (fragmentClass) { - return; - } - - // this require will apply the FragmentClass implementation - require('./fragment'); - - if (!fragmentClass) { - throw new Error('Failed to initialize the extended androidx.fragment.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; - private backgroundBitmap: android.graphics.Bitmap = null; - - @profile - public onHiddenChanged(fragment: androidx.fragment.app.Fragment, hidden: boolean, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onHiddenChanged(${hidden})`, Trace.categories.NativeLifecycle); - } - superFunc.call(fragment, hidden); - } - - @profile - public onCreateAnimator(fragment: androidx.fragment.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { - let animator = null; - const entry = this.entry; - - // Return enterAnimator only when new (no current entry) nested transition. - if (enter && entry.isNestedDefaultTransition) { - animator = entry.enterAnimator; - entry.isNestedDefaultTransition = false; - } - - return animator || superFunc.call(fragment, transit, enter, nextAnim); - } - - @profile - public onCreate(fragment: androidx.fragment.app.Fragment, savedInstanceState: android.os.Bundle, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onCreate(${savedInstanceState})`, Trace.categories.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 args = fragment.getArguments(); - const frameId = args.getInt(FRAMEID); - const frame = getFrameByNumberId(frameId); - if (!frame) { - throw new Error(`Cannot find Frame for ${fragment}`); - } - - findPageForFragment(fragment, frame); - } - } - - @profile - public onCreateView(fragment: androidx.fragment.app.Fragment, inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle, superFunc: Function): android.view.View { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onCreateView(inflater, container, ${savedInstanceState})`, Trace.categories.NativeLifecycle); - } - - const entry = this.entry; - if (!entry) { - Trace.error(`${fragment}.onCreateView: entry is null or undefined`); - - return null; - } - - const page = entry.resolvedPage; - if (!page) { - Trace.error(`${fragment}.onCreateView: entry has no resolvedPage`); - - return null; - } - - const frame = this.frame; - if (!frame) { - Trace.error(`${fragment}.onCreateView: this.frame is null or undefined`); - - return null; - } - - frame._resolvedPage = page; - - if (page.parent === frame) { - frame._inheritStyles(page); - - // If we are navigating to a page that was destroyed - // reinitialize its UI. - if (!page._context) { - const context = (container && container.getContext()) || (inflater && inflater.getContext()); - page._setupUI(context); - } - - if (frame.isLoaded && !page.isLoaded) { - page.callLoaded(); - } - } else { - if (!page.parent) { - if (!frame._styleScope) { - // Make sure page will have styleScope even if parents don't. - page._updateStyleScope(); - } - - frame._addView(page); - } else { - throw new Error('Page is already shown on another frame.'); - } - } - - const savedState = entry.viewSavedState; - if (savedState) { - (page.nativeViewProtected).restoreHierarchyState(savedState); - entry.viewSavedState = null; - } - - // fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'. - // on app resume in nested frame scenarios with support library version greater than 26.0.0 - // HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent - // right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a - // parent while its supposed parent believes it properly removed its children; in order to "force" the child to - // lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass) - const nativeView = page.nativeViewProtected; - if (nativeView != null) { - const parentView = nativeView.getParent(); - if (parentView instanceof android.view.ViewGroup) { - if (parentView.getChildCount() === 0) { - parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams()); - } - - parentView.removeAllViews(); - } - } - - return page.nativeViewProtected; - } - - @profile - public onSaveInstanceState(fragment: androidx.fragment.app.Fragment, outState: android.os.Bundle, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onSaveInstanceState(${outState})`, Trace.categories.NativeLifecycle); - } - superFunc.call(fragment, outState); - } - - @profile - public onDestroyView(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { - try { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onDestroyView()`, Trace.categories.NativeLifecycle); - } - - const hasRemovingParent = fragment.getRemovingParentFragment(); - - if (hasRemovingParent) { - const nativeFrameView = this.frame.nativeViewProtected; - if (nativeFrameView) { - const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(Application.android.context.getResources(), this.backgroundBitmap); - this.frame._originalBackground = this.frame.backgroundColor || new Color('White'); - nativeFrameView.setBackgroundDrawable(bitmapDrawable); - this.backgroundBitmap = null; - } - } - } finally { - superFunc.call(fragment); - } - } - - @profile - public onDestroy(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write(`${fragment}.onDestroy()`, Trace.categories.NativeLifecycle); - } - - superFunc.call(fragment); - - const entry = this.entry; - if (!entry) { - Trace.error(`${fragment}.onDestroy: entry is null or undefined`); - - return null; - } - - // [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629 - // retaining reference to a destroyed fragment here somehow causes a cryptic - // "IllegalStateException: Failure saving state: active fragment has cleared index: -1" - // in a specific mixed parent / nested frame navigation scenario - entry.fragment = null; - - const page = entry.resolvedPage; - if (!page) { - // todo: check why this happens when using shared element transition!!! - // commented out the Trace.error to prevent a crash (the app will still work interestingly) - console.log(`${fragment}.onDestroy: entry has no resolvedPage`); - // Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`); - - return null; - } - } - - @profile - public onPause(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { - try { - // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TODO: Consider removing it when update to androidx.fragment:1.2.0 - const hasRemovingParent = fragment.getRemovingParentFragment(); - - if (hasRemovingParent) { - this.backgroundBitmap = this.loadBitmapFromView(this.frame.nativeViewProtected); - } - } finally { - superFunc.call(fragment); - } - } - - @profile - public onResume(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { - const frame = this.entry.resolvedPage.frame; - // 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 - // so we manually set the entry - // also, to be compatible with fragments 1.2.x we need this setTimeout as animations haven't run on onResume yet - const weakRef = new WeakRef(this); - setTimeout(() => { - const owner = weakRef.get(); - if (!owner) { - return; - } - if (frame._executingContext && !(owner.entry).isAnimationRunning) { - frame.setCurrent(owner.entry, frame._executingContext.navigationType); - } - }, 0); - - superFunc.call(fragment); - } - - @profile - public onStop(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { - superFunc.call(fragment); - } - - @profile - public toStringOverride(fragment: androidx.fragment.app.Fragment, superFunc: Function): string { - const entry = this.entry; - if (entry) { - return `${entry.fragmentTag}<${entry.resolvedPage}>`; - } else { - return 'NO ENTRY, ' + superFunc.call(fragment); - } - } - - private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { - // Don't try to create bitmaps with no dimensions as this causes a crash - // This might happen when showing and closing dialogs fast. - if (!(view && view.getWidth() > 0 && view.getHeight() > 0)) { - return undefined; - } - - // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled - // const width = view.getWidth(); - // const height = view.getHeight(); - // const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); - // const canvas = new android.graphics.Canvas(bitmap); - // view.layout(0, 0, width, height); - // view.draw(canvas); - - // view.setDrawingCacheEnabled(true); - // const drawCache = view.getDrawingCache(); - // const bitmap = android.graphics.Bitmap.createBitmap(drawCache); - // view.setDrawingCacheEnabled(false); - return org.nativescript.widgets.Utils.getBitmapFromView(view); - } -} - -class ActivityCallbacksImplementation implements AndroidActivityCallbacks { - private _rootView: View; - - public getRootView(): View { - return this._rootView; - } - - @profile - public onCreate(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, intentOrSuperFunc: android.content.Intent | Function, superFunc?: Function): void { - if (Trace.isEnabled()) { - Trace.write(`Activity.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle); - } - - const intent: android.content.Intent = superFunc ? intentOrSuperFunc : undefined; - - if (!superFunc) { - console.log('AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, superFunc: Function) ' + 'is deprecated. Use AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function) instead.'); - superFunc = intentOrSuperFunc; - } - - // 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 moduleLoaded 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. - const isRestart = !!savedInstanceState && moduleLoaded; - superFunc.call(activity, isRestart ? savedInstanceState : null); - - // Try to get the rootViewId form the saved state in case the activity - // was destroyed and we are now recreating it. - if (savedInstanceState) { - const rootViewId = savedInstanceState.getInt(ROOT_VIEW_ID_EXTRA, -1); - if (rootViewId !== -1 && activityRootViewsMap.has(rootViewId)) { - this._rootView = activityRootViewsMap.get(rootViewId)?.get(); - } - } - - if (intent && intent.getAction()) { - Application.android.notify({ - eventName: Application.AndroidApplication.activityNewIntentEvent, - object: Application.android, - activity, - intent, - }); - } - - this.setActivityContent(activity, savedInstanceState, true); - moduleLoaded = true; - } - - @profile - public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void { - superFunc.call(activity, outState); - const rootView = this._rootView; - if (rootView instanceof Frame) { - outState.putInt(INTENT_EXTRA, rootView.android.frameId); - rootView._saveFragmentsState(); - } - - if (rootView) { - outState.putInt(ROOT_VIEW_ID_EXTRA, rootView._domId); - } - } - - @profile - public onNewIntent(activity: androidx.appcompat.app.AppCompatActivity, intent: android.content.Intent, superSetIntentFunc: Function, superFunc: Function): void { - superFunc.call(activity, intent); - superSetIntentFunc.call(activity, intent); - - Application.android.notify({ - eventName: Application.AndroidApplication.activityNewIntentEvent, - object: Application.android, - activity, - intent, - }); - } - - @profile - public onStart(activity: any, superFunc: Function): void { - superFunc.call(activity); - - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onStart();', Trace.categories.NativeLifecycle); - } - - const rootView = this._rootView; - if (rootView && !rootView.isLoaded) { - rootView.callLoaded(); - } - } - - @profile - public onStop(activity: any, superFunc: Function): void { - superFunc.call(activity); - - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle); - } - - const rootView = this._rootView; - if (rootView && rootView.isLoaded) { - rootView.callUnloaded(); - } - } - - @profile - public onPostResume(activity: any, superFunc: Function): void { - superFunc.call(activity); - - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onPostResume();', Trace.categories.NativeLifecycle); - } - - // NOTE: activity.onPostResume() is called when activity resume is complete and we can - // safely raise the application resume event; - // onActivityResumed(...) lifecycle callback registered in application is called too early - // and raising the application resume event there causes issues like - // https://github.com/NativeScript/NativeScript/issues/6708 - if ((activity).isNativeScriptActivity) { - Application.setSuspended(false, { - // todo: deprecate in favor of using event.activity instead. - android: activity, - activity, - }); - } - } - - @profile - public onDestroy(activity: any, superFunc: Function): void { - try { - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle); - } - - const rootView = this._rootView; - if (rootView) { - rootView._tearDownUI(true); - } - - // this may happen when the user changes the system theme - // In such case, isFinishing() is false (and isChangingConfigurations is true), and the app will start again (onCreate) with a savedInstanceState - // as a result, launchEvent will never be called - // possible alternative: always fire launchEvent and exitEvent, but pass extra flags to make it clear what kind of launch/destroy is happening - if (activity.isFinishing()) { - const exitArgs = { - eventName: Application.exitEvent, - object: Application.android, - android: activity, - }; - Application.notify(exitArgs); - } - } finally { - superFunc.call(activity); - } - } - - @profile - public onBackPressed(activity: any, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onBackPressed;', Trace.categories.NativeLifecycle); - } - - const args = { - eventName: 'activityBackPressed', - object: Application, - android: Application.android, - activity: activity, - cancel: false, - }; - Application.android.notify(args); - if (args.cancel) { - return; - } - - const view = this._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 ? !FrameBase.goBack() : true; - } - - if (callSuper) { - superFunc.call(activity); - } - } - - @profile - public onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array, grantResults: Array, superFunc: Function): void { - if (Trace.isEnabled()) { - Trace.write('NativeScriptActivity.onRequestPermissionsResult;', Trace.categories.NativeLifecycle); - } - - Application.android.notify({ - eventName: 'activityRequestPermissions', - object: Application, - android: 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 (Trace.isEnabled()) { - Trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, Trace.categories.NativeLifecycle); - } - - Application.android.notify({ - eventName: 'activityResult', - object: Application, - android: Application.android, - activity: activity, - requestCode: requestCode, - resultCode: resultCode, - intent: data, - }); - } - - public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity): void { - if (this._rootView) { - const manager = this._rootView._getFragmentManager(); - manager.executePendingTransactions(); - - this._rootView._onRootViewReset(); - } - // Delete previously cached root view in order to recreate it. - this._rootView = null; - this.setActivityContent(activity, null, false); - this._rootView.callLoaded(); - } - - // Paths that go trough this method: - // 1. Application initial start - there is no rootView in callbacks. - // 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate. - // 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired - // 4. resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired - private setActivityContent(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean): void { - let rootView = this._rootView; - - if (Trace.isEnabled()) { - Trace.write(`Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, Trace.categories.NativeLifecycle); - } - - const intent = activity.getIntent(); - rootView = Application.createRootView(rootView, fireLaunchEvent, { - // todo: deprecate in favor of args.intent? - android: intent, - intent, - savedInstanceState, - }); - - if (!rootView) { - // no root view created - return; - } - - activityRootViewsMap.set(rootView._domId, new WeakRef(rootView)); - - // setup view as styleScopeHost - rootView._setupAsRootView(activity); - - activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams()); - - this._rootView = rootView; - - // sets root classes once rootView is ready... - Application.initRootView(rootView); - } -} - export function setActivityCallbacks(activity: androidx.appcompat.app.AppCompatActivity): void { activity[CALLBACKS] = new ActivityCallbacksImplementation(); } diff --git a/packages/core/ui/frame/index.d.ts b/packages/core/ui/frame/index.d.ts index 521154806d..7f71bc8364 100644 --- a/packages/core/ui/frame/index.d.ts +++ b/packages/core/ui/frame/index.d.ts @@ -17,6 +17,16 @@ export interface NavigationData extends EventData { * Nested frames are supported, enabling hierarchical navigation scenarios. */ export class Frame extends FrameBase { + /** + * @private + */ + _originalBackground?: any; + + /** + * @private + */ + _saveFragmentsState?(); + /** * Gets a frame by id. */ @@ -432,6 +442,7 @@ export interface BackstackEntry { * To start a new Activity, a new Frame instance should be created and navigated to the desired Page. */ export interface AndroidFrame extends Observable { + frameId: any; /** * Gets the native [android ViewGroup](http://developer.android.com/reference/android/view/ViewGroup.html) instance that represents the root layout part of the Frame. */ diff --git a/packages/webpack5/src/platforms/android.ts b/packages/webpack5/src/platforms/android.ts index 4e7b6718b3..7ca5b0babb 100644 --- a/packages/webpack5/src/platforms/android.ts +++ b/packages/webpack5/src/platforms/android.ts @@ -2,6 +2,9 @@ import { INativeScriptPlatform } from "../helpers/platform"; import { env } from '../'; function getDistPath() { + if (process.env.USER_PROJECT_PLATFORMS_ANDROID) { + return `${process.env.USER_PROJECT_PLATFORMS_ANDROID}/${process.env.USER_PROJECT_PLATFORMS_ANDROID_MODULE}/src/nativescript/assets/app`; + } return `${env.buildPath ?? "platforms"}/android/app/src/main/assets/app`; } diff --git a/packages/webpack5/src/platforms/ios.ts b/packages/webpack5/src/platforms/ios.ts index 2ae91ba982..25d2457a02 100644 --- a/packages/webpack5/src/platforms/ios.ts +++ b/packages/webpack5/src/platforms/ios.ts @@ -11,7 +11,8 @@ function sanitizeName(appName: string): string { } function getDistPath() { const appName = sanitizeName(basename(getProjectRootPath())); - return `${env.buildPath ?? "platforms"}/ios/${appName}/app`; + const platform = process.env.USER_PROJECT_PLATFORMS_IOS ? process.env.USER_PROJECT_PLATFORMS_IOS : `${env.buildPath ?? "platforms"}/ios`; + return `${platform}/${appName}/app`; } const iOSPlatform: INativeScriptPlatform = {