diff --git a/apps/automated/src/file-system/file-system-tests.ts b/apps/automated/src/file-system/file-system-tests.ts index e502418a5..aed9e5da4 100644 --- a/apps/automated/src/file-system/file-system-tests.ts +++ b/apps/automated/src/file-system/file-system-tests.ts @@ -425,11 +425,11 @@ export var testFileNameExtension = function () { var file = documents.getFile('Test.txt'); // Getting the file name "Test.txt". var fileName = file.name; - // Getting the file extension ".txt". + // Getting the file extension "txt". var fileExtension = file.extension; // >> (hide) TKUnit.assert(fileName === 'Test.txt', 'Wrong file name.'); - TKUnit.assert(fileExtension === '.txt', 'Wrong extension.'); + TKUnit.assert(fileExtension === 'txt', 'Wrong extension.'); file.remove(); // << (hide) // << file-system-extension @@ -633,7 +633,7 @@ export function test_FSEntity_Properties() { var documents = fs.knownFolders.documents(); var file = documents.getFile('Test_File.txt'); - TKUnit.assert(file.extension === '.txt', 'FileEntity.extension not working.'); + TKUnit.assert(file.extension === 'txt', 'FileEntity.extension not working.'); TKUnit.assert(file.isLocked === false, 'FileEntity.isLocked not working.'); TKUnit.assert(file.lastModified instanceof Date, 'FileEntity.lastModified not working.'); TKUnit.assert(file.size === 0, 'FileEntity.size not working.'); diff --git a/packages/core/file-system/file-system-access.android.ts b/packages/core/file-system/file-system-access.android.ts index 70514e38b..1a935ccb6 100644 --- a/packages/core/file-system/file-system-access.android.ts +++ b/packages/core/file-system/file-system-access.android.ts @@ -1,5 +1,6 @@ import * as textModule from '../text'; -import { getFileExtension, android as androidUtils } from '../utils'; +import { android as androidUtils } from '../utils'; +import { getFileExtension } from '../utils/utils-shared'; import { SDK_VERSION } from '../utils/constants'; import type { IFileSystemAccess } from './file-system-access'; diff --git a/packages/core/file-system/file-system-access.ios.ts b/packages/core/file-system/file-system-access.ios.ts index 295cf3ba4..7e23da551 100644 --- a/packages/core/file-system/file-system-access.ios.ts +++ b/packages/core/file-system/file-system-access.ios.ts @@ -1,5 +1,6 @@ import { encoding as textEncoding } from '../text'; import { ios as iosUtils } from '../utils'; +import { getFileExtension } from '../utils/utils-shared'; // TODO: Implement all the APIs receiving callback using async blocks // TODO: Check whether we need try/catch blocks for the iOS implementation @@ -611,24 +612,8 @@ export class FileSystemAccess { return url.path; } - // TODO: This method is the same as in the iOS implementation. - // Make it in a separate file / module so it can be reused from both implementations. public getFileExtension(path: string): string { - // TODO [For Panata]: The definitions currently specify "any" as a return value of this method - //const nsString = Foundation.NSString.stringWithString(path); - //const extension = nsString.pathExtension(); - - //if (extension && extension.length > 0) { - // extension = extension.concat(".", extension); - //} - - //return extension; - const dotIndex = path.lastIndexOf('.'); - if (dotIndex && dotIndex >= 0 && dotIndex < path.length) { - return path.substring(dotIndex); - } - - return ''; + return getFileExtension(path); } private deleteEntity(path: string, onError?: (error: any) => any) { diff --git a/packages/core/http/http-request/index.android.ts b/packages/core/http/http-request/index.android.ts index ec96a8da9..52aa3b269 100644 --- a/packages/core/http/http-request/index.android.ts +++ b/packages/core/http/http-request/index.android.ts @@ -1,7 +1,7 @@ // imported for definition purposes only -import * as httpModule from '../../http'; +import type { Headers, HttpResponse, HttpRequestOptions } from '../../http'; import { ImageSource } from '../../image-source'; -import { Screen } from '../../platform'; +import { Screen } from '../../platform/screen'; import { File } from '../../file-system'; import { HttpResponseEncoding } from '../http-interfaces'; import { getFilenameFromUrl } from './http-request-common'; @@ -51,7 +51,7 @@ function onRequestComplete(requestId: number, result: org.nativescript.widgets.A } // read the headers - const headers: httpModule.Headers = {}; + const headers: Headers = {}; if (result.headers) { const jHeaders = result.headers; const length = jHeaders.size(); @@ -177,7 +177,7 @@ function onRequestError(error: string, requestId: number) { } } -function buildJavaOptions(options: httpModule.HttpRequestOptions) { +function buildJavaOptions(options: HttpRequestOptions) { if (typeof options.url !== 'string') { throw new Error('Http request must provide a valid url.'); } @@ -224,13 +224,13 @@ function buildJavaOptions(options: httpModule.HttpRequestOptions) { return javaOptions; } -export function request(options: httpModule.HttpRequestOptions): Promise { +export function request(options: HttpRequestOptions): Promise { if (options === undefined || options === null) { // TODO: Shouldn't we throw an error here - defensive programming return; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { // initialize the options const javaOptions = buildJavaOptions(options); @@ -289,7 +289,7 @@ function decodeResponse(raw: any, encoding?: HttpResponseEncoding) { return raw.toString(charsetName); } -export function addHeader(headers: httpModule.Headers, key: string, value: string): void { +export function addHeader(headers: Headers, key: string, value: string): void { if (!headers[key]) { headers[key] = value; } else if (Array.isArray(headers[key])) { diff --git a/packages/core/http/http-request/index.ios.ts b/packages/core/http/http-request/index.ios.ts index c7448bd68..96019a975 100644 --- a/packages/core/http/http-request/index.ios.ts +++ b/packages/core/http/http-request/index.ios.ts @@ -1,5 +1,5 @@ import { SDK_VERSION } from '../../utils/constants'; -import { isRealDevice } from '../../utils'; +import { isRealDevice } from '../../utils/native-helper'; import * as types from '../../utils/types'; import * as domainDebugger from '../../debugger'; import { getFilenameFromUrl } from './http-request-common'; diff --git a/packages/core/js-libs/easysax/easysax.js b/packages/core/js-libs/easysax/easysax.js index 3d11cf2a7..c2cf62784 100644 --- a/packages/core/js-libs/easysax/easysax.js +++ b/packages/core/js-libs/easysax/easysax.js @@ -63,7 +63,7 @@ // module.exports.EasySAXParser = EasySAXParser; // }; -export function EasySAXParser() { +function EasySAXParser() { 'use strict'; if (!this) return null; @@ -780,3 +780,5 @@ EasySAXParser.prototype.parse = function(xml) { j += 1; }; }; + +export { EasySAXParser }; \ No newline at end of file diff --git a/packages/core/media-query-list/index.ts b/packages/core/media-query-list/index.ts index f15e42de4..31f540a9c 100644 --- a/packages/core/media-query-list/index.ts +++ b/packages/core/media-query-list/index.ts @@ -1,5 +1,5 @@ import { EventData, Observable } from '../data/observable'; -import { Screen } from '../platform'; +import { Screen } from '../platform/screen'; import { getApplicationProperties, toggleApplicationEventListeners } from '../application/helpers-common'; import type { ApplicationEventData } from '../application/application-interfaces'; import { matchQuery, MediaQueryType } from '../css-mediaquery'; diff --git a/packages/core/ui/frame/fragment.android.ts b/packages/core/ui/frame/fragment.android.ts index 1fb760208..0e114ae12 100644 --- a/packages/core/ui/frame/fragment.android.ts +++ b/packages/core/ui/frame/fragment.android.ts @@ -1,5 +1,5 @@ import { isEmbedded, getEmbeddedView } from '../embedding'; -import { setFragmentCallbacks } from '.'; +import { setFragmentCallbacks } from './frame-helper-for-android'; declare const com: any; diff --git a/packages/core/ui/frame/frame-helper-for-android.ts b/packages/core/ui/frame/frame-helper-for-android.ts new file mode 100644 index 000000000..4cbd8ffb6 --- /dev/null +++ b/packages/core/ui/frame/frame-helper-for-android.ts @@ -0,0 +1,342 @@ +import { Trace } from '../../trace'; +import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions } from './fragment.transitions'; +import type { BackstackEntry } from '.'; +import { profile } from '../../profiling'; +import { getNativeApp } from '../../application/helpers-common'; +import { Color } from '../../color'; +import type { Page } from '../page'; +import type { AndroidFrame as Frame } from '.'; +export const FRAMEID = '_frameId'; +export const CALLBACKS = '_callbacks'; +export const framesCache = new Array>(); + +export interface AndroidFragmentCallbacks { + onHiddenChanged(fragment: any, hidden: boolean, superFunc: Function): void; + onCreateAnimator(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; + onCreate(fragment: any, savedInstanceState: any, superFunc: Function): void; + onCreateView(fragment: any, inflater: any, container: any, savedInstanceState: any, superFunc: Function): any; + onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; + onDestroyView(fragment: any, superFunc: Function): void; + onDestroy(fragment: any, superFunc: Function): void; + onPause(fragment: any, superFunc: Function): void; + onResume(fragment: any, superFunc: Function): void; + onStop(fragment: any, superFunc: Function): void; + toStringOverride(fragment: any, superFunc: Function): string; +} + +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}.`); + } +} + +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; + + // @ts-ignore + if (page.parent === frame) { + (frame as 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 as 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((getNativeApp() as android.app.Application).getApplicationContext().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); + } +} + +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(); + if (aliveFrame && aliveFrame.frameId === frameId) { + return aliveFrame.owner; + } + } + + return null; +} + +export function setFragmentCallbacks(fragment: androidx.fragment.app.Fragment): void { + fragment[CALLBACKS] = new FragmentCallbacksImplementation(); +} diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 7839b4454..fc5881041 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -11,18 +11,16 @@ import { android as androidUtils } from '../../utils/native-helper'; import type { ExpandedEntry } from './fragment.transitions.android'; import { ensureFragmentClass, fragmentClass } from './fragment'; import { getAppMainEntry } from '../../application/helpers-common'; -import { getNativeApp } from '../../application/helpers-common'; -import { Color } from '../../color'; + import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData } from '../../application/application-interfaces'; import { Application } from '../../application/application'; import { isEmbedded, setEmbeddedView } from '../embedding'; +import { CALLBACKS, FRAMEID, framesCache, setFragmentCallbacks } from './frame-helper-for-android'; export * from './frame-common'; export { setFragmentClass } from './fragment'; const INTENT_EXTRA = 'com.tns.activity'; -const FRAMEID = '_frameId'; -const CALLBACKS = '_callbacks'; const ownerSymbol = Symbol('_owner'); @@ -648,7 +646,6 @@ function restoreTransitionState(entry: BackstackEntry, snapshot: TransitionState } let framesCounter = 0; -const framesCache = new Array>(); class AndroidFrame extends Observable implements AndroidFrameDefinition { public rootViewGroup: android.view.ViewGroup; @@ -760,18 +757,6 @@ function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frame activity.startActivity(intent); } -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(); - if (aliveFrame && aliveFrame.frameId === frameId) { - return aliveFrame.owner; - } - } - - return null; -} - const activityRootViewsMap = new Map>(); const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId'; @@ -1063,310 +1048,6 @@ export class ActivityCallbacksImplementation implements AndroidActivityCallbacks } } -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((getNativeApp() as android.app.Application).getApplicationContext().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}.`); - } -} - export function setActivityCallbacks(activity: androidx.appcompat.app.AppCompatActivity): void { activity[CALLBACKS] = new ActivityCallbacksImplementation(); } - -export function setFragmentCallbacks(fragment: androidx.fragment.app.Fragment): void { - fragment[CALLBACKS] = new FragmentCallbacksImplementation(); -} diff --git a/packages/core/ui/frame/index.d.ts b/packages/core/ui/frame/index.d.ts index e02aa51cf..d3cafe153 100644 --- a/packages/core/ui/frame/index.d.ts +++ b/packages/core/ui/frame/index.d.ts @@ -1,8 +1,8 @@ -import { NavigationType, FrameBase } from './frame-common'; +import type { NavigationType, FrameBase } from './frame-common'; import type { NavigatedData, Page } from '../page'; -import { Observable, EventData } from '../../data/observable'; -import { Property, View } from '../core/view'; -import { Transition } from '../transition'; +import type { Observable, EventData } from '../../data/observable'; +import type { Property, View } from '../core/view'; +import type { Transition } from '../transition'; export * from './frame-interfaces'; @@ -521,6 +521,19 @@ export interface AndroidFrame extends Observable { * @param page The Page instance to search for. */ fragmentForPage(entry: BackstackEntry): any; + + // common properties + _resolvedPage?: Page; + _currentEntry?: BackstackEntry; + _executingContext?: NavigationContext; + _inheritStyles?(page: Page): void; + isLoaded?: boolean; + _styleScope?: any; + _addView?(view: View): void; + nativeViewProtected?: any /* android.view.View */; + _originalBackground?: any /* android.graphics.drawable.Drawable */; + backgroundColor?: any; + owner?: any; } export interface AndroidActivityCallbacks { @@ -539,20 +552,6 @@ export interface AndroidActivityCallbacks { onNewIntent(activity: any, intent: any, superSetIntentFunc: Function, superFunc: Function): void; } -export interface AndroidFragmentCallbacks { - onHiddenChanged(fragment: any, hidden: boolean, superFunc: Function): void; - onCreateAnimator(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; - onCreate(fragment: any, savedInstanceState: any, superFunc: Function): void; - onCreateView(fragment: any, inflater: any, container: any, savedInstanceState: any, superFunc: Function): any; - onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; - onDestroyView(fragment: any, superFunc: Function): void; - onDestroy(fragment: any, superFunc: Function): void; - onPause(fragment: any, superFunc: Function): void; - onResume(fragment: any, superFunc: Function): void; - onStop(fragment: any, superFunc: Function): void; - toStringOverride(fragment: any, superFunc: Function): string; -} - /* tslint:disable */ /** * Represents the iOS-specific Frame object, aggregated within the common Frame one. diff --git a/packages/core/ui/styling/length-shared.ts b/packages/core/ui/styling/length-shared.ts index 003d26c54..77efcbe7c 100644 --- a/packages/core/ui/styling/length-shared.ts +++ b/packages/core/ui/styling/length-shared.ts @@ -2,7 +2,7 @@ // Only put platform-agnostic logic here. import { CoreTypes } from '../../core-types'; -import { layout } from '../../utils'; +import { layout } from '../../utils/layout-helper'; import { isCssWideKeyword } from '../core/properties/property-shared'; function equalsCommon(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean; diff --git a/packages/core/utils/common.ts b/packages/core/utils/common.ts index bd9855497..82a1a1387 100644 --- a/packages/core/utils/common.ts +++ b/packages/core/utils/common.ts @@ -4,6 +4,7 @@ import emojiRegex from 'emoji-regex'; export * from './mainthread-helper'; export * from './macrotask-scheduler'; +export * from './utils-shared'; export const RESOURCE_PREFIX = 'res://'; export const SYSTEM_PREFIX = 'sys://'; @@ -164,5 +165,3 @@ export function isEmoji(value: string): boolean { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes return emojiRegex().test(value); } - -export { getFileExtension } from './utils-shared'; diff --git a/packages/core/utils/index.android.ts b/packages/core/utils/index.android.ts index 787a445e2..60369b904 100644 --- a/packages/core/utils/index.android.ts +++ b/packages/core/utils/index.android.ts @@ -1,5 +1,5 @@ import { Trace } from '../trace'; -import { getFileExtension } from './common'; +import { getFileExtension } from './utils-shared'; import { SDK_VERSION } from './constants'; import { android as AndroidUtils } from './native-helper'; import { topmost } from '../ui/frame/frame-stack'; diff --git a/packages/core/utils/native-helper-for-android.ts b/packages/core/utils/native-helper-for-android.ts new file mode 100644 index 000000000..cd0bf59a5 --- /dev/null +++ b/packages/core/utils/native-helper-for-android.ts @@ -0,0 +1,296 @@ +import { numberHasDecimals, numberIs64Bit } from './types'; +import { getNativeApp } from '../application/helpers-common'; +import { androidGetCurrentActivity } from '../application/helpers'; +import { Trace } from '../trace'; +import { topmost } from '../ui/frame/frame-stack'; + +export function dataDeserialize(nativeData?: any) { + if (nativeData === null || typeof nativeData !== 'object') { + return nativeData; + } + let store; + + switch (nativeData.getClass().getName()) { + case 'java.lang.String': { + return String(nativeData); + } + + case 'java.lang.Boolean': { + return String(nativeData) === 'true'; + } + + case 'java.lang.Float': + case 'java.lang.Integer': + case 'java.lang.Long': + case 'java.lang.Double': + case 'java.lang.Short': { + return Number(nativeData); + } + + case 'org.json.JSONArray': { + store = []; + for (let j = 0; j < nativeData.length(); j++) { + store[j] = dataDeserialize(nativeData.get(j)); + } + break; + } + case 'org.json.JSONObject': { + store = {}; + const i = nativeData.keys(); + let key; + while (i.hasNext()) { + key = i.next(); + store[key] = dataDeserialize(nativeData.get(key)); + } + break; + } + + case 'androidx.collection.SimpleArrayMap': { + const count = nativeData.size(); + for (let l = 0; l < count; l++) { + const key = nativeData.keyAt(l); + store[key] = dataDeserialize(nativeData.get(key)); + } + break; + } + + case 'androidx.collection.ArrayMap': + case 'android.os.Bundle': + case 'java.util.HashMap': + case 'java.util.Map': { + store = {}; + const keys = nativeData.keySet().toArray(); + for (let k = 0; k < keys.length; k++) { + const key = keys[k]; + store[key] = dataDeserialize(nativeData.get(key)); + } + break; + } + + default: + if (typeof nativeData === 'object' && nativeData instanceof java.util.List) { + const array = []; + const size = nativeData.size(); + for (let i = 0, n = size; i < n; i++) { + array[i] = dataDeserialize(nativeData.get(i)); + } + store = array; + } else { + store = null; + } + break; + } + return store; +} + +export function dataSerialize(data?: any, wrapPrimitives?: boolean) { + let store; + switch (typeof data) { + case 'string': + case 'boolean': { + if (wrapPrimitives) { + if (typeof data === 'string') { + return new java.lang.String(data); + } + return new java.lang.Boolean(data); + } + return data; + } + case 'number': { + const hasDecimals = numberHasDecimals(data); + if (numberIs64Bit(data)) { + if (hasDecimals) { + return java.lang.Double.valueOf(data); + } else { + return java.lang.Long.valueOf(data); + } + } else { + if (hasDecimals) { + return java.lang.Float.valueOf(data); + } else { + return java.lang.Integer.valueOf(data); + } + } + } + + case 'object': { + if (!data) { + return null; + } + + if (data instanceof Date) { + return new java.util.Date(data.getTime()); + } + + if (Array.isArray(data)) { + store = new java.util.ArrayList(); + data.forEach((item) => store.add(dataSerialize(item, wrapPrimitives))); + return store; + } + + if (data.native) { + return data.native; + } + + store = new java.util.HashMap(); + Object.keys(data).forEach((key) => store.put(key, dataSerialize(data[key], wrapPrimitives))); + return store; + } + + default: + return null; + } +} + +export function getApplicationContext() { + return getApplication().getApplicationContext(); +} +export function getCurrentActivity() { + return androidGetCurrentActivity(); +} +export function getApplication() { + return getNativeApp() as android.app.Application; +} +export function getResources() { + return getApplication().getResources(); +} +let packageName: string; +export function getPackageName() { + if (!packageName) { + packageName = getApplicationContext().getPackageName(); + } + + return packageName; +} + +let inputMethodManager: android.view.inputmethod.InputMethodManager; +export function getInputMethodManager(): android.view.inputmethod.InputMethodManager { + if (!inputMethodManager) { + inputMethodManager = getApplicationContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); + } + + return inputMethodManager; +} + +export function getWindow() { + return getCurrentActivity()?.getWindow(); +} + +export function showSoftInput(nativeView: android.view.View): void { + const inputManager = getInputMethodManager(); + if (inputManager && nativeView instanceof android.view.View) { + inputManager.showSoftInput(nativeView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); + } +} + +export function dismissSoftInput(nativeView?: android.view.View): void { + const inputManager = getInputMethodManager(); + let windowToken: android.os.IBinder; + + if (nativeView instanceof android.view.View) { + if (!nativeView.hasFocus()) { + return; + } + windowToken = nativeView.getWindowToken(); + } else if (getCurrentActivity() instanceof androidx.appcompat.app.AppCompatActivity) { + const modalDialog = (topmost()?._modalParent ?? (topmost()?.modal as any))?._dialogFragment?.getDialog(); + const window = (modalDialog ?? getCurrentActivity()).getWindow(); + const decorView = window.getDecorView(); + if (decorView) { + windowToken = decorView.getWindowToken(); + decorView.requestFocus(); + } else { + windowToken = null; + } + } + + if (inputManager && windowToken) { + inputManager.hideSoftInputFromWindow(windowToken, 0); + } +} + +export namespace collections { + export function stringArrayToStringSet(str: string[]): java.util.HashSet { + const hashSet = new java.util.HashSet(); + if (str !== undefined) { + for (const element in str) { + hashSet.add('' + str[element]); + } + } + + return hashSet; + } + + export function stringSetToStringArray(stringSet: any): string[] { + const arr = []; + if (stringSet !== undefined) { + const it = stringSet.iterator(); + while (it.hasNext()) { + const element = '' + it.next(); + arr.push(element); + } + } + + return arr; + } +} + +export namespace resources { + let attr; + const attrCache = new Map(); + + export function getDrawableId(name) { + return getId(':drawable/' + name); + } + + export function getStringId(name) { + return getId(':string/' + name); + } + + export function getId(name: string): number { + const resources = getResources(); + const packageName = getPackageName(); + const uri = packageName + name; + + return resources.getIdentifier(uri, null, null); + } + export function getResource(name: string, type?: string): number { + return getResources().getIdentifier(name, type, getPackageName()); + } + export function getPaletteColor(name: string, context: android.content.Context): number { + if (attrCache.has(name)) { + return attrCache.get(name); + } + + let result = 0; + try { + if (!attr) { + attr = java.lang.Class.forName('androidx.appcompat.R$attr'); + } + + let colorID = 0; + const field = attr.getField(name); + if (field) { + colorID = field.getInt(null); + } + + if (colorID) { + const typedValue = new android.util.TypedValue(); + context.getTheme().resolveAttribute(colorID, typedValue, true); + result = typedValue.data; + } + } catch (ex) { + Trace.write('Cannot get pallete color: ' + name, Trace.categories.Error, Trace.messageType.error); + } + + attrCache.set(name, result); + + return result; + } +} + +export function isRealDevice(): boolean { + const fingerprint = android.os.Build.FINGERPRINT; + + return fingerprint != null && (fingerprint.indexOf('vbox') > -1 || fingerprint.indexOf('generic') > -1); +} diff --git a/packages/core/utils/native-helper.android.ts b/packages/core/utils/native-helper.android.ts index b1e03a3b8..2fac473b7 100644 --- a/packages/core/utils/native-helper.android.ts +++ b/packages/core/utils/native-helper.android.ts @@ -1,300 +1,7 @@ import { platformCheck } from './platform-check'; -import { numberHasDecimals, numberIs64Bit } from './types'; -import { getNativeApp } from '../application/helpers-common'; -import { androidGetCurrentActivity } from '../application/helpers'; -import { Trace } from '../trace'; -import { topmost } from '../ui/frame/frame-stack'; -export function dataDeserialize(nativeData?: any) { - if (nativeData === null || typeof nativeData !== 'object') { - return nativeData; - } - let store; - - switch (nativeData.getClass().getName()) { - case 'java.lang.String': { - return String(nativeData); - } - - case 'java.lang.Boolean': { - return String(nativeData) === 'true'; - } - - case 'java.lang.Float': - case 'java.lang.Integer': - case 'java.lang.Long': - case 'java.lang.Double': - case 'java.lang.Short': { - return Number(nativeData); - } - - case 'org.json.JSONArray': { - store = []; - for (let j = 0; j < nativeData.length(); j++) { - store[j] = dataDeserialize(nativeData.get(j)); - } - break; - } - case 'org.json.JSONObject': { - store = {}; - const i = nativeData.keys(); - let key; - while (i.hasNext()) { - key = i.next(); - store[key] = dataDeserialize(nativeData.get(key)); - } - break; - } - - case 'androidx.collection.SimpleArrayMap': { - const count = nativeData.size(); - for (let l = 0; l < count; l++) { - const key = nativeData.keyAt(l); - store[key] = dataDeserialize(nativeData.get(key)); - } - break; - } - - case 'androidx.collection.ArrayMap': - case 'android.os.Bundle': - case 'java.util.HashMap': - case 'java.util.Map': { - store = {}; - const keys = nativeData.keySet().toArray(); - for (let k = 0; k < keys.length; k++) { - const key = keys[k]; - store[key] = dataDeserialize(nativeData.get(key)); - } - break; - } - - default: - if (typeof nativeData === 'object' && nativeData instanceof java.util.List) { - const array = []; - const size = nativeData.size(); - for (let i = 0, n = size; i < n; i++) { - array[i] = dataDeserialize(nativeData.get(i)); - } - store = array; - } else { - store = null; - } - break; - } - return store; -} - -export function dataSerialize(data?: any, wrapPrimitives?: boolean) { - let store; - switch (typeof data) { - case 'string': - case 'boolean': { - if (wrapPrimitives) { - if (typeof data === 'string') { - return new java.lang.String(data); - } - return new java.lang.Boolean(data); - } - return data; - } - case 'number': { - const hasDecimals = numberHasDecimals(data); - if (numberIs64Bit(data)) { - if (hasDecimals) { - return java.lang.Double.valueOf(data); - } else { - return java.lang.Long.valueOf(data); - } - } else { - if (hasDecimals) { - return java.lang.Float.valueOf(data); - } else { - return java.lang.Integer.valueOf(data); - } - } - } - - case 'object': { - if (!data) { - return null; - } - - if (data instanceof Date) { - return new java.util.Date(data.getTime()); - } - - if (Array.isArray(data)) { - store = new java.util.ArrayList(); - data.forEach((item) => store.add(dataSerialize(item, wrapPrimitives))); - return store; - } - - if (data.native) { - return data.native; - } - - store = new java.util.HashMap(); - Object.keys(data).forEach((key) => store.put(key, dataSerialize(data[key], wrapPrimitives))); - return store; - } - - default: - return null; - } -} - -function getApplicationContext() { - return getApplication().getApplicationContext(); -} -function getCurrentActivity() { - return androidGetCurrentActivity(); -} -function getApplication() { - return getNativeApp() as globalAndroid.app.Application; -} -function getResources() { - return getApplication().getResources(); -} -let packageName: string; -function getPackageName() { - if (!packageName) { - packageName = getApplicationContext().getPackageName(); - } - - return packageName; -} - -let inputMethodManager: globalAndroid.view.inputmethod.InputMethodManager; -function getInputMethodManager(): globalAndroid.view.inputmethod.InputMethodManager { - if (!inputMethodManager) { - inputMethodManager = getApplicationContext().getSystemService(globalAndroid.content.Context.INPUT_METHOD_SERVICE); - } - - return inputMethodManager; -} - -export function getWindow() { - return getCurrentActivity()?.getWindow(); -} - -function showSoftInput(nativeView: globalAndroid.view.View): void { - const inputManager = getInputMethodManager(); - if (inputManager && nativeView instanceof globalAndroid.view.View) { - inputManager.showSoftInput(nativeView, globalAndroid.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); - } -} - -function dismissSoftInput(nativeView?: globalAndroid.view.View): void { - const inputManager = getInputMethodManager(); - let windowToken: globalAndroid.os.IBinder; - - if (nativeView instanceof globalAndroid.view.View) { - if (!nativeView.hasFocus()) { - return; - } - windowToken = nativeView.getWindowToken(); - } else if (getCurrentActivity() instanceof androidx.appcompat.app.AppCompatActivity) { - const modalDialog = (topmost()?._modalParent ?? (topmost()?.modal as any))?._dialogFragment?.getDialog(); - const window = (modalDialog ?? getCurrentActivity()).getWindow(); - const decorView = window.getDecorView(); - if (decorView) { - windowToken = decorView.getWindowToken(); - decorView.requestFocus(); - } else { - windowToken = null; - } - } - - if (inputManager && windowToken) { - inputManager.hideSoftInputFromWindow(windowToken, 0); - } -} - -namespace collections { - export function stringArrayToStringSet(str: string[]): java.util.HashSet { - const hashSet = new java.util.HashSet(); - if (str !== undefined) { - for (const element in str) { - hashSet.add('' + str[element]); - } - } - - return hashSet; - } - - export function stringSetToStringArray(stringSet: any): string[] { - const arr = []; - if (stringSet !== undefined) { - const it = stringSet.iterator(); - while (it.hasNext()) { - const element = '' + it.next(); - arr.push(element); - } - } - - return arr; - } -} - -namespace resources { - let attr; - const attrCache = new Map(); - - export function getDrawableId(name) { - return getId(':drawable/' + name); - } - - export function getStringId(name) { - return getId(':string/' + name); - } - - export function getId(name: string): number { - const resources = getResources(); - const packageName = getPackageName(); - const uri = packageName + name; - - return resources.getIdentifier(uri, null, null); - } - export function getResource(name: string, type?: string): number { - return getResources().getIdentifier(name, type, getPackageName()); - } - export function getPaletteColor(name: string, context: globalAndroid.content.Context): number { - if (attrCache.has(name)) { - return attrCache.get(name); - } - - let result = 0; - try { - if (!attr) { - attr = java.lang.Class.forName('androidx.appcompat.R$attr'); - } - - let colorID = 0; - const field = attr.getField(name); - if (field) { - colorID = field.getInt(null); - } - - if (colorID) { - const typedValue = new globalAndroid.util.TypedValue(); - context.getTheme().resolveAttribute(colorID, typedValue, true); - result = typedValue.data; - } - } catch (ex) { - Trace.write('Cannot get pallete color: ' + name, Trace.categories.Error, Trace.messageType.error); - } - - attrCache.set(name, result); - - return result; - } -} - -export function isRealDevice(): boolean { - const fingerprint = globalAndroid.os.Build.FINGERPRINT; - - return fingerprint != null && (fingerprint.indexOf('vbox') > -1 || fingerprint.indexOf('generic') > -1); -} +// importing this helper as a separate file avoids "android" symbol clash with the global android object +import { resources, getApplication, getCurrentActivity, getApplicationContext, getWindow, getResources, getPackageName, getInputMethodManager, showSoftInput, dismissSoftInput } from './native-helper-for-android'; export const android = { resources, diff --git a/packages/core/utils/utils-shared.ts b/packages/core/utils/utils-shared.ts index 35bec0906..ba76655a1 100644 --- a/packages/core/utils/utils-shared.ts +++ b/packages/core/utils/utils-shared.ts @@ -1,6 +1,3 @@ -// Shared helpers and types for utils, used by both native-helper and common. -// Only put platform-agnostic logic here. - export function getFileExtension(path: string): string { if (!path) { return ''; @@ -8,5 +5,3 @@ export function getFileExtension(path: string): string { const index = path.lastIndexOf('.'); return index !== -1 ? path.substring(index + 1) : ''; } - -// Add more shared helpers/types/constants as needed. diff --git a/packages/core/xhr/index.ts b/packages/core/xhr/index.ts index 32df8072a..46324d453 100644 --- a/packages/core/xhr/index.ts +++ b/packages/core/xhr/index.ts @@ -1,5 +1,6 @@ -import * as http from '../http'; -import * as types from '../utils/types'; +import type { HttpRequestOptions, HttpResponse } from '../http'; +import { request } from '../http'; +import { isString, isFunction } from '../utils/types'; import { Trace } from '../trace'; namespace XMLHttpRequestResponseType { @@ -24,7 +25,7 @@ export class XMLHttpRequest { public onloadstart: (...args: any[]) => void; public onprogress: (...args: any[]) => void; - private _options: http.HttpRequestOptions; + private _options: HttpRequestOptions; private _readyState: number; private _status: number; private _response: any; @@ -64,7 +65,7 @@ export class XMLHttpRequest { throw new Error("Failed to read the 'responseText' property from 'XMLHttpRequest': " + "The value is only accessible if the object's 'responseType' is '' or 'text' " + `(was '${this._responseType}').`); } - if (types.isFunction(this._responseTextReader)) { + if (isFunction(this._responseTextReader)) { return this._responseTextReader(); } @@ -103,7 +104,7 @@ export class XMLHttpRequest { this._readyState = this.UNSENT; } - private _loadResponse(r: http.HttpResponse) { + private _loadResponse(r: HttpResponse) { this._status = r.statusCode; this._headers = r.headers; this._setReadyState(this.HEADERS_RECEIVED); @@ -135,7 +136,7 @@ export class XMLHttpRequest { } private emitEvent(eventName: string, ...args: Array) { - if (types.isFunction(this['on' + eventName])) { + if (isFunction(this['on' + eventName])) { this['on' + eventName](...args); } @@ -188,15 +189,15 @@ export class XMLHttpRequest { } public open(method: string, url: string, async?: boolean, user?: string, password?: string) { - if (types.isString(method) && types.isString(url)) { + if (isString(method) && isString(url)) { this._options = { url: url, method: method }; this._options.headers = {}; - if (types.isString(user)) { + if (isString(user)) { this._options.headers['user'] = user; } - if (types.isString(password)) { + if (isString(password)) { this._options.headers['password'] = password; } @@ -232,7 +233,7 @@ export class XMLHttpRequest { throw new Error("Failed to execute 'send' on 'XMLHttpRequest': " + "The object's state must be OPENED."); } - if (types.isString(data) && this._options.method !== 'GET') { + if (isString(data) && this._options.method !== 'GET') { //The Android Java HTTP lib throws an exception if we provide a //a request body for GET requests, so we avoid doing that. //Browser implementations silently ignore it as well. @@ -250,8 +251,7 @@ export class XMLHttpRequest { this.emitEvent('loadstart'); - http - .request(this._options) + request(this._options) .then((r) => { if (!this._errorFlag && this._sendFlag) { this._loadResponse(r); @@ -269,7 +269,7 @@ export class XMLHttpRequest { throw new Error("Failed to execute 'setRequestHeader' on 'XMLHttpRequest': " + "The object's state must be OPENED."); } - if (types.isString(header) && types.isString(value)) { + if (isString(header) && isString(value)) { this._options.headers[header] = value; } } @@ -289,7 +289,7 @@ export class XMLHttpRequest { } public getResponseHeader(header: string): string { - if (types.isString(header) && this._readyState > 1 && this._headers && !this._errorFlag) { + if (isString(header) && this._readyState > 1 && this._headers && !this._errorFlag) { header = header.toLowerCase(); for (const i in this._headers) { if (i.toLowerCase() === header) { @@ -559,7 +559,7 @@ export class FileReader { } private emitEvent(eventName: string, ...args: Array) { - if (types.isFunction(this['on' + eventName])) { + if (isFunction(this['on' + eventName])) { this['on' + eventName](...args); }