diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index 265c20fbd..3c75a31ad 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -105,8 +105,8 @@ export type { InstrumentationMode, TimerInfo } from './profiling'; export { encoding } from './text'; export * from './trace'; export * from './ui'; -import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, queueMacrotask, queueGC, throttle, debounce } from './utils'; -import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback } from './utils/types'; +import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, queueMacrotask, queueGC, throttle, debounce, dataSerialize, dataDeserialize } from './utils'; +import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types'; export declare const Utils: { GC: typeof GC; RESOURCE_PREFIX: string; @@ -134,6 +134,10 @@ export declare const Utils: { android: typeof androidUtils; ad: typeof androidUtils; ios: typeof iosUtils; + dataSerialize: typeof dataSerialize; + dataDeserialize: typeof dataDeserialize; + numberHasDecimals: typeof numberHasDecimals; + numberIs64Bit: typeof numberIs64Bit; setTimeout: typeof setTimeout; setInterval: typeof setInterval; clearInterval: typeof clearInterval; diff --git a/packages/core/index.ts b/packages/core/index.ts index 5786c31bd..7c45937a9 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -137,8 +137,8 @@ export * from './trace'; export * from './ui'; -import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput } from './utils'; -import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback } from './utils/types'; +import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput, dataDeserialize, dataSerialize } from './utils'; +import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types'; export const Utils = { GC, @@ -170,6 +170,10 @@ export const Utils = { // legacy (a lot of plugins use the shorthand "ad" Utils.ad instead of Utils.android) ad: androidUtils, ios: iosUtils, + dataSerialize, + dataDeserialize, + numberHasDecimals, + numberIs64Bit, setTimeout, setInterval, clearInterval, diff --git a/packages/core/utils/native-helper.android.ts b/packages/core/utils/native-helper.android.ts index cdb68a71b..978793ce9 100644 --- a/packages/core/utils/native-helper.android.ts +++ b/packages/core/utils/native-helper.android.ts @@ -1,5 +1,143 @@ import { getNativeApplication, android as androidApp } from '../application'; import { Trace } from '../trace'; +import { numberHasDecimals, numberIs64Bit } from './types'; + +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 = {}; + let i = nativeData.keys(); + while (i.hasNext()) { + let 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; + } +} // We are using "ad" here to avoid namespace collision with the global android object export namespace ad { diff --git a/packages/core/utils/native-helper.d.ts b/packages/core/utils/native-helper.d.ts index 339bc9320..8fed91420 100644 --- a/packages/core/utils/native-helper.d.ts +++ b/packages/core/utils/native-helper.d.ts @@ -1,3 +1,14 @@ +/** + * Data serialization from JS > Native + * @param wrapPrimitives Optionally wrap primitive types (Some APIs may require this) + */ +export function dataSerialize(data?: any, wrapPrimitives?: boolean): any; +/** + * Data deserialization from Native > JS + * @param nativeData Native platform data + */ +export function dataDeserialize(nativeData?: any): any; + /** * Module with android specific utilities. */ diff --git a/packages/core/utils/native-helper.ios.ts b/packages/core/utils/native-helper.ios.ts index a685a32c5..d01d85532 100644 --- a/packages/core/utils/native-helper.ios.ts +++ b/packages/core/utils/native-helper.ios.ts @@ -1,5 +1,5 @@ import { Trace } from '../trace'; -import { getClass, isNullOrUndefined } from './types'; +import { getClass, isNullOrUndefined, numberHasDecimals, numberIs64Bit } from './types'; declare let UIImagePickerControllerSourceType: any; @@ -25,6 +25,88 @@ function openFileAtRootModule(filePath: string): boolean { return false; } +export function dataDeserialize(nativeData?: any) { + if (isNullOrUndefined(nativeData)) { + // some native values will already be js null values + // calling types.getClass below on null/undefined will cause crash + return null; + } else { + switch (getClass(nativeData)) { + case 'NSNull': + return null; + case 'NSMutableDictionary': + case 'NSDictionary': + let obj = {}; + const length = nativeData.count; + const keysArray = nativeData.allKeys as NSArray; + for (let i = 0; i < length; i++) { + const nativeKey = keysArray.objectAtIndex(i); + obj[nativeKey] = dataDeserialize(nativeData.objectForKey(nativeKey)); + } + return obj; + case 'NSMutableArray': + case 'NSArray': + let array = []; + const len = nativeData.count; + for (let i = 0; i < len; i++) { + array[i] = dataDeserialize(nativeData.objectAtIndex(i)); + } + return array; + default: + return nativeData; + } + } +} + +export function dataSerialize(data: any, wrapPrimitives: boolean = false) { + switch (typeof data) { + case 'string': + case 'boolean': { + return data; + } + case 'number': { + const hasDecimals = numberHasDecimals(data); + if (numberIs64Bit(data)) { + if (hasDecimals) { + return NSNumber.alloc().initWithDouble(data); + } else { + return NSNumber.alloc().initWithLongLong(data); + } + } else { + if (hasDecimals) { + return NSNumber.alloc().initWithFloat(data); + } else { + return data; + } + } + } + + case 'object': { + if (data instanceof Date) { + return NSDate.dateWithTimeIntervalSince1970(data.getTime() / 1000); + } + + if (!data) { + return null; + } + + if (Array.isArray(data)) { + return NSArray.arrayWithArray((data).map(dataSerialize)); + } + + let node = {} as any; + Object.keys(data).forEach(function (key) { + let value = data[key]; + node[key] = dataSerialize(value, wrapPrimitives); + }); + return NSDictionary.dictionaryWithDictionary(node); + } + + default: + return null; + } +} + export namespace iOSNativeHelper { // TODO: remove for NativeScript 7.0 export function getter(_this: any, property: T | { (): T }): T { @@ -54,60 +136,6 @@ export namespace iOSNativeHelper { } } - export function dataDeserialize(nativeData?: any) { - if (isNullOrUndefined(nativeData)) { - // some native values will already be js null values - // calling types.getClass below on null/undefined will cause crash - return null; - } else { - switch (getClass(nativeData)) { - case 'NSNull': - return null; - case 'NSMutableDictionary': - case 'NSDictionary': - let obj = {}; - const length = nativeData.count; - const keysArray = nativeData.allKeys as NSArray; - for (let i = 0; i < length; i++) { - const nativeKey = keysArray.objectAtIndex(i); - obj[nativeKey] = dataDeserialize(nativeData.objectForKey(nativeKey)); - } - return obj; - case 'NSMutableArray': - case 'NSArray': - let array = []; - const len = nativeData.count; - for (let i = 0; i < len; i++) { - array[i] = dataDeserialize(nativeData.objectAtIndex(i)); - } - return array; - default: - return nativeData; - } - } - } - - export function dataSerialize(data?: any) { - switch (typeof data) { - case 'number': - case 'string': - case 'boolean': - return data; - case 'object': - if (Array.isArray(data)) { - return NSArray.arrayWithArray(data.map(dataSerialize)); - } - - let obj = {}; - for (let key of Object.keys(data)) { - obj[key] = dataSerialize(data[key]); - } - return NSDictionary.dictionaryWithDictionary(obj); - default: - return NSNull.new(); - } - } - export function isLandscape(): boolean { console.log('utils.ios.isLandscape() is deprecated; use application.orientation instead'); diff --git a/packages/core/utils/types.d.ts b/packages/core/utils/types.d.ts index a5edb773e..9425edc43 100644 --- a/packages/core/utils/types.d.ts +++ b/packages/core/utils/types.d.ts @@ -61,6 +61,18 @@ export function isNullOrUndefined(value: any): boolean; */ export function verifyCallback(value: any): void; +/** + * Checks if the number has decimals + * @param value Number to check + */ +export function numberHasDecimals(value: number): boolean; + +/** + * Checks if the number is 64 bit + * @param value Number to check + */ +export function numberIs64Bit(value: number): boolean; + /** * A function that gets the class name of an object. * @param object The object. diff --git a/packages/core/utils/types.ts b/packages/core/utils/types.ts index 7109579ef..1fba0ae3a 100644 --- a/packages/core/utils/types.ts +++ b/packages/core/utils/types.ts @@ -44,6 +44,14 @@ export function verifyCallback(value: any) { } } +export function numberHasDecimals(value: number): boolean { + return !(value % 1 === 0); +} + +export function numberIs64Bit(value: number): boolean { + return value < -Math.pow(2, 31) + 1 || value > Math.pow(2, 31) - 1; +} + const classInfosMap = new Map(); // ES3-5 type classes are "function blah()", new ES6+ classes can be "class blah"