From c60f74d4eb4e37c97fc92fd3065f8966e37a0bb7 Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Wed, 8 May 2019 13:56:45 +0300 Subject: [PATCH 1/5] fix(devtools-ios): Ensure UI modifications run on main thread Modifications to the UI can only be made from the main thread. Since {N} 5.3.0 all debugger protocol messages are processed by the worker thread that receives them in iOS. refs #7219, https://github.com/NativeScript/ios-runtime/pull/1101 --- tests/app/utils/utils-tests.ts | 32 +++++++++++++++++++ .../debugger/devtools-elements.common.ts | 15 +++++---- tns-core-modules/utils/utils-common.ts | 8 +++++ tns-core-modules/utils/utils.android.ts | 11 +++++++ tns-core-modules/utils/utils.d.ts | 20 ++++++++++++ tns-core-modules/utils/utils.ios.ts | 8 +++++ 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/tests/app/utils/utils-tests.ts b/tests/app/utils/utils-tests.ts index 10e07d498..3dba797a2 100644 --- a/tests/app/utils/utils-tests.ts +++ b/tests/app/utils/utils-tests.ts @@ -18,6 +18,38 @@ export function test_releaseNativeObject_canBeCalledWithNativeObject() { } }; + +export function test_executeOnMainThread_Works(done: Function) { + utils.executeOnMainThread(() => { + try { + TKUnit.assertTrue(utils.isMainThread()); + done(); + } catch (e) { + done(e); + } + }); +} + +export function test_mainThreadify_PassesArgs(done: Function) { + const expectedN = 434; + const expectedB = true; + const expectedS = "string"; + const f = utils.mainThreadify(function (n: number, b: boolean, s: string) { + try { + TKUnit.assertTrue(utils.isMainThread()); + TKUnit.assertEqual(n, expectedN); + TKUnit.assertEqual(b, expectedB); + TKUnit.assertEqual(s, expectedS); + done(); + } catch (e) { + done(e); + } + }); + + f(expectedN, expectedB, expectedS); +} + + function test_releaseNativeObject_canBeCalledWithNativeObject_iOS() { let deallocated = false; const obj = new ((NSObject).extend({ diff --git a/tns-core-modules/debugger/devtools-elements.common.ts b/tns-core-modules/debugger/devtools-elements.common.ts index 186cfe320..c0b7ac43f 100644 --- a/tns-core-modules/debugger/devtools-elements.common.ts +++ b/tns-core-modules/debugger/devtools-elements.common.ts @@ -2,6 +2,7 @@ import { getNodeById } from "./dom-node"; // Needed for typings only import { ViewBase } from "../ui/core/view-base"; +import { mainThreadify } from "../utils/utils"; // Use lazy requires for core modules const frameTopmost = () => require("../ui/frame").topmost(); @@ -11,7 +12,7 @@ function unsetViewValue(view, name) { if (!unsetValue) { unsetValue = require("../ui/core/properties").unsetValue; } - + view[name] = unsetValue; } @@ -30,10 +31,10 @@ export function getDocument() { if (!topMostFrame) { return undefined; } - + try { topMostFrame.ensureDomNode(); - + } catch (e) { console.log("ERROR in getDocument(): " + e); } @@ -49,7 +50,7 @@ export function getComputedStylesForNode(nodeId): Array<{ name: string, value: s return []; } -export function removeNode(nodeId) { +export const removeNode = mainThreadify(function removeNode(nodeId) { const view = getViewById(nodeId); if (view) { // Avoid importing layout and content view @@ -63,9 +64,9 @@ export function removeNode(nodeId) { console.log("Can't remove child from " + parent); } } -} +}); -export function setAttributeAsText(nodeId, text, name) { +export const setAttributeAsText = mainThreadify(function setAttributeAsText(nodeId, text, name) { const view = getViewById(nodeId); if (view) { // attribute is registered for the view instance @@ -93,4 +94,4 @@ export function setAttributeAsText(nodeId, text, name) { view.domNode.loadAttributes(); } -} +}); diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index 97829e166..d94022b0f 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -1,4 +1,5 @@ import * as types from "./types"; +import { executeOnMainThread } from "./utils" export const RESOURCE_PREFIX = "res://"; export const FILE_PREFIX = "file:///"; @@ -154,3 +155,10 @@ export function hasDuplicates(arr: Array): boolean { export function eliminateDuplicates(arr: Array): Array { return Array.from(new Set(arr)); } + +export function mainThreadify(func: Function): (...args: any[]) => void { + return function () { + const argsToPass = arguments; + executeOnMainThread(() => func.apply(this, argsToPass)); + } +} diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index b050042f9..196dedcda 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -372,3 +372,14 @@ Please ensure you have your manifest correctly configured with the FileProvider. return false; } } + +export function executeOnMainThread(func: () => void) { + new android.os.Handler(android.os.Looper.getMainLooper()) + .post(new java.lang.Runnable({ + run: func + })); +} + +export function isMainThread(): Boolean { + return android.os.Looper.myLooper() === android.os.Looper.getMainLooper(); +} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 6058a7508..308eefc0b 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -269,6 +269,26 @@ export function GC(); */ export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/); +/** + * Dispatches the passed function for execution on the main thread + * @param func The function to execute on the main thread. + */ +export function executeOnMainThread(func: Function); + +/** + * Returns a function wrapper which executes the supplied function on the main thread. + * The wrapper behaves like the original function and passes all of its arguments BUT + * discards its return value. + * @param func The function to execute on the main thread + * @returns The wrapper function which schedules execution to the main thread + */ +export function mainThreadify(func: Function): (...args: any[]) => void + +/** + * @returns Boolean value indicating whether the current thread is the main thread + */ +export function isMainThread(): boolean + /** * Returns true if the specified path points to a resource or local file. * @param path The path. diff --git a/tns-core-modules/utils/utils.ios.ts b/tns-core-modules/utils/utils.ios.ts index 941782f16..95ccbe318 100644 --- a/tns-core-modules/utils/utils.ios.ts +++ b/tns-core-modules/utils/utils.ios.ts @@ -159,6 +159,14 @@ export function openUrl(location: string): boolean { return false; } +export function executeOnMainThread(func: () => void) { + NSOperationQueue.mainQueue.addOperationWithBlock(func); +} + +export function isMainThread(): Boolean { + return NSThread.isMainThread; +} + class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; From a2ef6cbb4ba86910dd60b34afc1b3bacd0e36859 Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Wed, 8 May 2019 14:22:06 +0300 Subject: [PATCH 2/5] refact(utils): Check current thread and dispatch only if needed --- tests/app/utils/utils-tests.ts | 13 +++++++++++-- tns-core-modules/utils/utils-common.ts | 10 +++++++++- tns-core-modules/utils/utils.android.ts | 2 +- tns-core-modules/utils/utils.d.ts | 7 +++++++ tns-core-modules/utils/utils.ios.ts | 2 +- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/tests/app/utils/utils-tests.ts b/tests/app/utils/utils-tests.ts index 3dba797a2..ac9dd2ab0 100644 --- a/tests/app/utils/utils-tests.ts +++ b/tests/app/utils/utils-tests.ts @@ -18,7 +18,6 @@ export function test_releaseNativeObject_canBeCalledWithNativeObject() { } }; - export function test_executeOnMainThread_Works(done: Function) { utils.executeOnMainThread(() => { try { @@ -30,6 +29,17 @@ export function test_executeOnMainThread_Works(done: Function) { }); } +export function test_dispatchToMainThread_Works(done: Function) { + utils.dispatchToMainThread(() => { + try { + TKUnit.assertTrue(utils.isMainThread()); + done(); + } catch (e) { + done(e); + } + }); +} + export function test_mainThreadify_PassesArgs(done: Function) { const expectedN = 434; const expectedB = true; @@ -49,7 +59,6 @@ export function test_mainThreadify_PassesArgs(done: Function) { f(expectedN, expectedB, expectedS); } - function test_releaseNativeObject_canBeCalledWithNativeObject_iOS() { let deallocated = false; const obj = new ((NSObject).extend({ diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index d94022b0f..de5df72af 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -1,5 +1,5 @@ import * as types from "./types"; -import { executeOnMainThread } from "./utils" +import { dispatchToMainThread, isMainThread } from "./utils" export const RESOURCE_PREFIX = "res://"; export const FILE_PREFIX = "file:///"; @@ -156,6 +156,14 @@ export function eliminateDuplicates(arr: Array): Array { return Array.from(new Set(arr)); } +export function executeOnMainThread(func: Function) { + if (isMainThread()) { + return func(); + } else { + dispatchToMainThread(func); + } +} + export function mainThreadify(func: Function): (...args: any[]) => void { return function () { const argsToPass = arguments; diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index 196dedcda..bd7d9b854 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -373,7 +373,7 @@ Please ensure you have your manifest correctly configured with the FileProvider. } } -export function executeOnMainThread(func: () => void) { +export function dispatchToMainThread(func: () => void) { new android.os.Handler(android.os.Looper.getMainLooper()) .post(new java.lang.Runnable({ run: func diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 308eefc0b..05d9e08d5 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -273,6 +273,13 @@ export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/) * Dispatches the passed function for execution on the main thread * @param func The function to execute on the main thread. */ +export function dispatchToMainThread(func: Function); + +/** + * Checks if the current thread is the main thread. Directly calls the passed function + * if it is, or dispatches it to the main thread otherwise. + * @param func The function to execute on the main thread. + */ export function executeOnMainThread(func: Function); /** diff --git a/tns-core-modules/utils/utils.ios.ts b/tns-core-modules/utils/utils.ios.ts index 95ccbe318..494a03a15 100644 --- a/tns-core-modules/utils/utils.ios.ts +++ b/tns-core-modules/utils/utils.ios.ts @@ -159,7 +159,7 @@ export function openUrl(location: string): boolean { return false; } -export function executeOnMainThread(func: () => void) { +export function dispatchToMainThread(func: () => void) { NSOperationQueue.mainQueue.addOperationWithBlock(func); } From 7aed770871598003ebe40445909e9bcc477a8d43 Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Wed, 8 May 2019 17:28:29 +0300 Subject: [PATCH 3/5] refact(utils): Resolve circular dependency without duplicating any code --- tns-core-modules/utils/mainthread-helper.android.ts | 10 ++++++++++ tns-core-modules/utils/mainthread-helper.d.ts | 10 ++++++++++ tns-core-modules/utils/mainthread-helper.ios.ts | 7 +++++++ tns-core-modules/utils/utils-common.ts | 4 +++- tns-core-modules/utils/utils.android.ts | 11 ----------- tns-core-modules/utils/utils.d.ts | 13 ++----------- tns-core-modules/utils/utils.ios.ts | 8 -------- 7 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 tns-core-modules/utils/mainthread-helper.android.ts create mode 100644 tns-core-modules/utils/mainthread-helper.d.ts create mode 100644 tns-core-modules/utils/mainthread-helper.ios.ts diff --git a/tns-core-modules/utils/mainthread-helper.android.ts b/tns-core-modules/utils/mainthread-helper.android.ts new file mode 100644 index 000000000..1ef05eaa5 --- /dev/null +++ b/tns-core-modules/utils/mainthread-helper.android.ts @@ -0,0 +1,10 @@ +export function dispatchToMainThread(func: () => void) { + new android.os.Handler(android.os.Looper.getMainLooper()) + .post(new java.lang.Runnable({ + run: func + })); +} + +export function isMainThread(): Boolean { + return android.os.Looper.myLooper() === android.os.Looper.getMainLooper(); +} diff --git a/tns-core-modules/utils/mainthread-helper.d.ts b/tns-core-modules/utils/mainthread-helper.d.ts new file mode 100644 index 000000000..46ff0142d --- /dev/null +++ b/tns-core-modules/utils/mainthread-helper.d.ts @@ -0,0 +1,10 @@ +/** + * Dispatches the passed function for execution on the main thread + * @param func The function to execute on the main thread. + */ +export function dispatchToMainThread(func: Function); + +/** + * @returns Boolean value indicating whether the current thread is the main thread + */ +export function isMainThread(): boolean diff --git a/tns-core-modules/utils/mainthread-helper.ios.ts b/tns-core-modules/utils/mainthread-helper.ios.ts new file mode 100644 index 000000000..01ed3bb17 --- /dev/null +++ b/tns-core-modules/utils/mainthread-helper.ios.ts @@ -0,0 +1,7 @@ +export function dispatchToMainThread(func: () => void) { + NSOperationQueue.mainQueue.addOperationWithBlock(func); +} + +export function isMainThread(): Boolean { + return NSThread.isMainThread; +} diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index de5df72af..6141d8fe5 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -1,5 +1,7 @@ import * as types from "./types"; -import { dispatchToMainThread, isMainThread } from "./utils" +import { dispatchToMainThread, isMainThread } from "./mainthread-helper" + +export * from "./mainthread-helper" export const RESOURCE_PREFIX = "res://"; export const FILE_PREFIX = "file:///"; diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index bd7d9b854..b050042f9 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -372,14 +372,3 @@ Please ensure you have your manifest correctly configured with the FileProvider. return false; } } - -export function dispatchToMainThread(func: () => void) { - new android.os.Handler(android.os.Looper.getMainLooper()) - .post(new java.lang.Runnable({ - run: func - })); -} - -export function isMainThread(): Boolean { - return android.os.Looper.myLooper() === android.os.Looper.getMainLooper(); -} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 05d9e08d5..08a1d80c6 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -4,6 +4,8 @@ import { dip, px } from "../ui/core/view"; +export * from "./mainthread-helper" + export const RESOURCE_PREFIX: string; export const FILE_PREFIX: string; @@ -269,12 +271,6 @@ export function GC(); */ export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/); -/** - * Dispatches the passed function for execution on the main thread - * @param func The function to execute on the main thread. - */ -export function dispatchToMainThread(func: Function); - /** * Checks if the current thread is the main thread. Directly calls the passed function * if it is, or dispatches it to the main thread otherwise. @@ -291,11 +287,6 @@ export function executeOnMainThread(func: Function); */ export function mainThreadify(func: Function): (...args: any[]) => void -/** - * @returns Boolean value indicating whether the current thread is the main thread - */ -export function isMainThread(): boolean - /** * Returns true if the specified path points to a resource or local file. * @param path The path. diff --git a/tns-core-modules/utils/utils.ios.ts b/tns-core-modules/utils/utils.ios.ts index 494a03a15..941782f16 100644 --- a/tns-core-modules/utils/utils.ios.ts +++ b/tns-core-modules/utils/utils.ios.ts @@ -159,14 +159,6 @@ export function openUrl(location: string): boolean { return false; } -export function dispatchToMainThread(func: () => void) { - NSOperationQueue.mainQueue.addOperationWithBlock(func); -} - -export function isMainThread(): Boolean { - return NSThread.isMainThread; -} - class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; From 2bb5ff414d964f90f4211715b227449e71d43794 Mon Sep 17 00:00:00 2001 From: Manol Donev Date: Thu, 9 May 2019 13:05:20 +0300 Subject: [PATCH 4/5] chore(android): disable test_ImageCache_ValidUrl on api19 --- tests/app/ui/image-cache/image-cache-tests.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/app/ui/image-cache/image-cache-tests.ts b/tests/app/ui/image-cache/image-cache-tests.ts index c6237b8ac..946cf426b 100644 --- a/tests/app/ui/image-cache/image-cache-tests.ts +++ b/tests/app/ui/image-cache/image-cache-tests.ts @@ -1,9 +1,19 @@ import * as imageCacheModule from "tns-core-modules/ui/image-cache"; import * as imageSource from "tns-core-modules/image-source"; import * as types from "tns-core-modules/utils/types"; +import { device } from "tns-core-modules/platform"; +import lazy from "tns-core-modules/utils/lazy"; + import * as TKUnit from "../../TKUnit"; +const sdkVersion = lazy(() => parseInt(device.sdkVersion)); + export const test_ImageCache_ValidUrl = function() { + // see https://github.com/NativeScript/NativeScript/issues/6643 + if (sdkVersion() < 20) { + return; + } + const cache = new imageCacheModule.Cache(); cache.maxRequests = 5; From 6511eeb5a29675c31c7a978d30dffb82411a1f31 Mon Sep 17 00:00:00 2001 From: vakrilov Date: Thu, 9 May 2019 14:39:23 +0300 Subject: [PATCH 5/5] chore: remove clashing typings --- e2e/animation/package.json | 1 - e2e/modal-navigation/package.json | 3 +-- e2e/nested-frame-navigation/package.json | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/e2e/animation/package.json b/e2e/animation/package.json index 00c40a851..b89e6fd59 100644 --- a/e2e/animation/package.json +++ b/e2e/animation/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@types/chai": "~4.1.7", "@types/mocha": "~5.2.5", - "@types/node": "~10.12.18", "babel-traverse": "6.26.0", "babel-types": "6.26.0", "babylon": "6.18.0", diff --git a/e2e/modal-navigation/package.json b/e2e/modal-navigation/package.json index c3ffbd134..b9fca7282 100644 --- a/e2e/modal-navigation/package.json +++ b/e2e/modal-navigation/package.json @@ -19,14 +19,13 @@ "devDependencies": { "@types/chai": "~4.1.7", "@types/mocha": "~5.2.5", - "@types/node": "^7.0.5", "mocha": "~5.2.0", "mochawesome": "~3.1.2", "nativescript-dev-appium": "next", "nativescript-dev-typescript": "next", "nativescript-dev-webpack": "next", - "tns-platform-declarations": "next", "rimraf": "^2.6.2", + "tns-platform-declarations": "next", "typescript": "^3.1.6" }, "scripts": { diff --git a/e2e/nested-frame-navigation/package.json b/e2e/nested-frame-navigation/package.json index e9754a49e..a433feba2 100644 --- a/e2e/nested-frame-navigation/package.json +++ b/e2e/nested-frame-navigation/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@types/chai": "~4.1.7", "@types/mocha": "~5.2.5", - "@types/node": "^7.0.5", "mocha": "~5.2.0", "mochawesome": "~3.1.2", "nativescript-dev-appium": "next",