diff --git a/.gitattributes b/.gitattributes index e69de29bb..dd3950f03 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1 @@ +platforms/android/test-app/runtime/src/main/libs/**/libv8_monolith.a filter=lfs diff=lfs merge=lfs -text diff --git a/platforms/android/.github/workflows/build_hermes.yml b/platforms/android/.github/workflows/build_hermes.yml new file mode 100644 index 000000000..75605e37c --- /dev/null +++ b/platforms/android/.github/workflows/build_hermes.yml @@ -0,0 +1,34 @@ +name: Build with Hermes + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Install dependencies + run: | + cd test-app/build-tools/jsparser + npm install + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with HERMES + run: ./gradlew -Pengine=HERMES + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release + path: | + ./dist_hermes + + diff --git a/platforms/android/.github/workflows/build_jsc.yml b/platforms/android/.github/workflows/build_jsc.yml new file mode 100644 index 000000000..6b5626228 --- /dev/null +++ b/platforms/android/.github/workflows/build_jsc.yml @@ -0,0 +1,34 @@ +name: Build with JSC + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Install dependencies + run: | + cd test-app/build-tools/jsparser + npm install + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with JSC + run: ./gradlew -Pengine=JSC + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release + path: | + ./dist_jsc + + diff --git a/platforms/android/.github/workflows/build_quickjs.yml b/platforms/android/.github/workflows/build_quickjs.yml new file mode 100644 index 000000000..231820e3b --- /dev/null +++ b/platforms/android/.github/workflows/build_quickjs.yml @@ -0,0 +1,34 @@ +name: Build with QuickJS + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Install dependencies + run: | + cd test-app/build-tools/jsparser + npm install + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with QUICKJS + run: ./gradlew -Pengine=QUICKJS + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release + path: | + ./dist_quickjs + + diff --git a/platforms/android/.github/workflows/build_v8.yml b/platforms/android/.github/workflows/build_v8.yml new file mode 100644 index 000000000..d864ba712 --- /dev/null +++ b/platforms/android/.github/workflows/build_v8.yml @@ -0,0 +1,34 @@ +name: Build with V8 + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Install dependencies + run: | + cd test-app/build-tools/jsparser + npm install + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with V8 + run: ./gradlew -Pengine=V8 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release + path: | + ./dist_v8 + + diff --git a/platforms/android/.github/workflows/release.yml b/platforms/android/.github/workflows/release.yml new file mode 100644 index 000000000..fe36daed1 --- /dev/null +++ b/platforms/android/.github/workflows/release.yml @@ -0,0 +1,219 @@ +on: + workflow_dispatch: + inputs: + publish: + description: 'If true, run publish after build (default: false)' + required: false + default: 'false' + run_tests: + description: 'If true, run gradle runtestsAndVerifyResults before builds (default: false)' + required: false + default: 'false' + custom_version: + description: 'Optional custom NPM version (overrides computed version).' + required: false + default: '' + enable_HERMES: + description: 'Enable HERMES in the matrix' + required: false + default: 'true' + enable_V8: + description: 'Enable V8 in the matrix' + required: false + default: 'true' + enable_QUICKS: + description: 'Enable QUICKS in the matrix' + required: false + default: 'true' + enable_JSC: + description: 'Enable JSC in the matrix' + required: false + default: 'true' + +jobs: + set-matrix: + name: Build matrix from inputs + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.emit.outputs.matrix }} + steps: + - name: Emit matrix JSON + id: emit + run: | + set -e + # Build the engine list based on workflow dispatch inputs. + engines=() + if [ "${{ github.event.inputs.enable_HERMES }}" != "false" ]; then engines+=("HERMES"); fi + if [ "${{ github.event.inputs.enable_V8 }}" != "false" ]; then engines+=("V8"); fi + if [ "${{ github.event.inputs.enable_QUICKS }}" != "false" ]; then engines+=("QUICKS"); fi + if [ "${{ github.event.inputs.enable_JSC }}" != "false" ]; then engines+=("JSC"); fi + + # If user disabled everything, fall back to a safe default (all engines). + if [ ${#engines[@]} -eq 0 ]; then + echo "All engines were disabled; defaulting to all engines." + engines=(HERMES V8 QUICKS JSC) + fi + + # Build a JSON array string like ["HERMES","V8"] + json="[" + for e in "${engines[@]}"; do + json="${json}\"${e}\"," + done + json="${json%,}" # remove trailing comma + json="${json}]" + + echo "Computed matrix JSON: $json" + echo "matrix=$json" >> $GITHUB_OUTPUT + + engine: + name: Test · Build · Publish (engine=${{ matrix.engine }}) + needs: set-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # fromJson converts the JSON string emitted by set-matrix into the matrix array + engine: ${{ fromJson(needs.set-matrix.outputs.matrix) }} + env: + JS_PARSER_DIR: test-app/build-tools/jsparser + ENGINE: ${{ matrix.engine }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + + - name: Setup Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Restore npm cache for jsparser + uses: actions/cache@v4 + with: + path: | + ~/.npm + ${{ github.workspace }}/${{ env.JS_PARSER_DIR }}/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('test-app/build-tools/jsparser/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies (jsparser) + working-directory: ${{ env.JS_PARSER_DIR }} + run: npm ci + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run runSbgTests (no emulator) for ${{ matrix.engine }} + if: ${{ github.event.inputs.run_tests == 'true' }} + run: | + echo "Running runSbgTests with ENGINE=${ENGINE}" + ./gradlew -Pengine="${ENGINE}" runSbgTests + env: + ENGINE: ${{ matrix.engine }} + + - name: Run runtestsAndVerifyResults inside emulator for ${{ matrix.engine }} + if: ${{ github.event.inputs.run_tests == 'true' }} + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + arch: x86_64 + target: google_apis + emulator-options: -no-window + script: | + echo "Running runtestsAndVerifyResults with ENGINE=${ENGINE}" + ./gradlew -Pengine="${ENGINE}" runtestsAndVerifyResults + env: + ENGINE: ${{ matrix.engine }} + + - name: Build with engine ${{ matrix.engine }} + run: | + echo "Building with ENGINE=${ENGINE}" + ./gradlew -Pengine="${ENGINE}" + env: + ENGINE: ${{ matrix.engine }} + + - name: Publish tarball (or add dist-tag if version already exists) + id: npm_publish + run: | + set -e + eng_lc="${ENG_LC}" + TARBALL="${TARBALL:-dist-${eng_lc}-${NPM_VERSION}.tgz}" + TAG="${eng_lc}-${NPM_VERSION}" + echo "Publishing tarball $TARBALL with npm tag $TAG (package version: $NPM_VERSION)..." + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc + + if npm publish "$TARBALL" --tag "$TAG" --provenance --access public; then + echo "npm publish succeeded for $TARBALL" + echo "PUBLISHED=true" >> $GITHUB_ENV + else + echo "npm publish failed; attempting to add dist-tag pointing to existing version" + npm dist-tag add @nativescript/android@"${NPM_VERSION}" "${TAG}" + echo "DIST_TAG_ADDED=true" >> $GITHUB_ENV + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + NPM_VERSION: ${{ env.NPM_VERSION }} + ENG_LC: ${{ env.ENG_LC }} + TARBALL: ${{ env.TARBALL }} + + - name: Create and push git tag for engine/version + if: always() + id: tag_release + run: | + set -e + eng_lc="${ENG_LC}" + TAG_NAME="${eng_lc}-v${NPM_VERSION}" + echo "Creating tag $TAG_NAME" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git rev-parse --verify "$TAG_NAME" >/dev/null 2>&1; then + git tag -d "$TAG_NAME" || true + fi + git tag -a "$TAG_NAME" -m "Release ${TAG_NAME}" + git push origin "refs/tags/${TAG_NAME}" || true + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV + + - name: Update CHANGELOG + id: changelog + uses: requarks/changelog-action@v1 + with: + useGitmojis: false + excludeTypes: build,docs,other,style,chore,perf,doc + token: ${{ secrets.GITHUB_TOKEN }} + tag: v${{ env.TAG_NAME }} + writeToFile: false + + - name: Create GitHub release for engine + uses: actions/create-release@v1 + if: always() + with: + tag_name: ${{ env.TAG_NAME }} + release_name: ${{ env.ENGINE }} ${{ env.NPM_VERSION }} + body: ${{ steps.changelog.outputs.changes }} + draft: false + prerelease: ${{ contains(env.NPM_VERSION, '-dev') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ENGINE: ${{ matrix.engine }} + NPM_VERSION: ${{ env.NPM_VERSION }} + + - name: Final status print + run: | + echo "Engine: ${ENGINE}" + echo "NPM_VERSION: ${NPM_VERSION}" + echo "Published: ${PUBLISHED:-false}" + echo "Dist-tag added: ${DIST_TAG_ADDED:-false}" + echo "Tag created: ${TAG_NAME:-none}" + env: + PUBLISHED: ${{ env.PUBLISHED }} + DIST_TAG_ADDED: ${{ env.DIST_TAG_ADDED }} + TAG_NAME: ${{ env.TAG_NAME }} \ No newline at end of file diff --git a/platforms/android/.gitignore b/platforms/android/.gitignore new file mode 100644 index 000000000..971706a7e --- /dev/null +++ b/platforms/android/.gitignore @@ -0,0 +1,21 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +dist +dist_v8 +dist_quickjs +dist_hermes +dist_jsc +dist_* \ No newline at end of file diff --git a/platforms/android/CHANGELOG.md b/platforms/android/CHANGELOG.md new file mode 100644 index 000000000..d8f549b9e --- /dev/null +++ b/platforms/android/CHANGELOG.md @@ -0,0 +1,1189 @@ +## [8.8.5](https://github.com/NativeScript/android/compare/v8.8.4...v8.8.5) (2024-09-30) + + +### Bug Fixes + +* prevent metadata offset overflow into array space and convert shorts to uints before addition ([9cfc349](https://github.com/NativeScript/android/commit/9cfc3493017243948b043a51f68b7c7bcab1e6b9)) + + + +## [8.8.4](https://github.com/NativeScript/android/compare/v8.8.3...v8.8.4) (2024-09-06) + + +### Bug Fixes + +* ensure same mtime for js and code cache to prevent loading old code caches ([#1822](https://github.com/NativeScript/android/issues/1822)) ([3d6e101](https://github.com/NativeScript/android/commit/3d6e10115227ad556e5bbe1764217716ab5bdac7)) + + + +## [8.8.3](https://github.com/NativeScript/android/compare/v8.8.2...v8.8.3) (2024-09-02) + + +### Bug Fixes + +* generate correct metadata when overflowing signed short values ([#1821](https://github.com/NativeScript/android/issues/1821)) ([c9fac4b](https://github.com/NativeScript/android/commit/c9fac4b19a952d4df651d3d6a8b0fa9c50f7c7db)) + + + +## [8.8.2](https://github.com/NativeScript/android/compare/v8.8.1...v8.8.2) (2024-07-22) + + +### Bug Fixes + +* config with multiple bundle ids ([#1816](https://github.com/NativeScript/android/issues/1816)) ([cdcfee2](https://github.com/NativeScript/android/commit/cdcfee266617472ac7f3ac59742b858ad093e46b)) + + + +## [8.8.1](https://github.com/NativeScript/android/compare/v8.8.0...v8.8.1) (2024-07-10) + + +### Features + +* Ada 2.9 ([#1814](https://github.com/NativeScript/android/issues/1814)) ([91accf9](https://github.com/NativeScript/android/commit/91accf9be1caf9ad2accb80bf9aca18efe4dd75a)) + + + +# [8.8.0](https://github.com/NativeScript/android/compare/v8.7.0...v8.8.0) (2024-07-09) + + +### Bug Fixes + +* correctly load ts_helpers.js in workers ([#1798](https://github.com/NativeScript/android/issues/1798)) ([31f8501](https://github.com/NativeScript/android/commit/31f8501bb902815cfed8e1cd123fe8b6de2cb757)) + + +### Features + +* Kotlin 2 + Gradle 8+ ([#1812](https://github.com/NativeScript/android/issues/1812)) ([d4b7164](https://github.com/NativeScript/android/commit/d4b716427934ebb4387a04842561d5b5d0e1fa3d)) + + + +# [8.7.0](https://github.com/NativeScript/android/compare/v8.7.0-rc.3...v8.7.0) (2024-04-08) + + + +# [8.7.0-rc.3](https://github.com/NativeScript/android/compare/v8.6.2...v8.7.0-rc.3) (2024-04-08) + + +### Bug Fixes + +* devtools namespace usage ([#1810](https://github.com/NativeScript/android/issues/1810)) ([5aaac57](https://github.com/NativeScript/android/commit/5aaac5788ff9abf1c043817e87c8e03eb61907c0)) +* dts-generator.jar path ([1120a32](https://github.com/NativeScript/android/commit/1120a3258d53f83b7b4dfe7e505234e2b0d6cd2b)) +* inspector and globals ([#1811](https://github.com/NativeScript/android/issues/1811)) ([79ebd18](https://github.com/NativeScript/android/commit/79ebd18f308cd86fa98784f14b5c3f5ac39d8c5f)) + + +### Features + +* bump ndk to r23c ([#1803](https://github.com/NativeScript/android/issues/1803)) ([3894959](https://github.com/NativeScript/android/commit/3894959e0b4fe31f61cfd9fa70d5e2b04a0f36ac)) +* devtools element/network inspectors ([#1808](https://github.com/NativeScript/android/issues/1808)) ([1470796](https://github.com/NativeScript/android/commit/1470796dc506f0d01e94fe117119dc217ff8c909)) +* migrate to faster maps and use runtime context ([#1793](https://github.com/NativeScript/android/issues/1793)) ([b248dc4](https://github.com/NativeScript/android/commit/b248dc4038d0c1a6af420447c713bc968431f97e)) +* update libzip to 1.10.1 ([#1805](https://github.com/NativeScript/android/issues/1805)) ([ee2e3e0](https://github.com/NativeScript/android/commit/ee2e3e0b87caf3cff4784f1464dd51b2923c6861)) +* use node module bindings like the iOS runtime ([#1795](https://github.com/NativeScript/android/issues/1795)) ([643958b](https://github.com/NativeScript/android/commit/643958b6a4c3698567edde3fd03052873b2644dc)) +* **WinterCG:** URL & URLSearchParams ([#1801](https://github.com/NativeScript/android/issues/1801)) ([4f3a0d7](https://github.com/NativeScript/android/commit/4f3a0d7f2de5f899779bd0fe9081390e6c4d24b2)) + + +### Reverts + +* Version.h changes ([9faa25d](https://github.com/NativeScript/android/commit/9faa25dda197d3da4f694ea59208309bb02e529c)) + + + +## [8.6.2](https://github.com/NativeScript/android/compare/v8.6.1...v8.6.2) (2023-10-10) + + + +## [8.6.1](https://github.com/NativeScript/android/compare/v8.6.0...v8.6.1) (2023-10-10) + + +### Bug Fixes + +* copy drawables ([4ff92cb](https://github.com/NativeScript/android/commit/4ff92cb32a954be4c3d32c302e301cef0a4b72a6)) + + + +# [8.6.0](https://github.com/NativeScript/android/compare/v8.5.3...v8.6.0) (2023-10-06) + + +### Bug Fixes + +* make jar files readonly prior to loading ([#1790](https://github.com/NativeScript/android/issues/1790)) ([2bcdaf0](https://github.com/NativeScript/android/commit/2bcdaf01fb850db4a982c22c2d792f9493a2a7fa)) +* only use project jar files if they are linked ([d23ca94](https://github.com/NativeScript/android/commit/d23ca94ba7c660b26224c57ba6f22085aa99f95c)) +* revert namespace change as to not break existing projects ([8b7b59d](https://github.com/NativeScript/android/commit/8b7b59d23d926b696bde3c1031cf3a842a24133d)) + + +### Features + +* improved error activity ui ([#1776](https://github.com/NativeScript/android/issues/1776)) ([ee3e354](https://github.com/NativeScript/android/commit/ee3e354f1bec89268daf93086aa6dd24898677b9)) +* upgrade client gradle version ([c778c0d](https://github.com/NativeScript/android/commit/c778c0d238c4ba44390f786ba06ab8e51ffb2c97)) + +## [8.5.4](https://github.com/NativeScript/android/compare/v8.5.3...v8.5.4) (2023-09-27) + + +### Bug Fixes + +* make jar files readonly prior to loading ([#1790](https://github.com/NativeScript/android/issues/1790)) ([14a932a](https://github.com/NativeScript/android/commit/14a932ad2d62c94f2f4e139125835da760dcdd58)) + + +## [8.5.3](https://github.com/NativeScript/android/compare/v8.5.2...v8.5.3) (2023-09-22) + + +### Bug Fixes + +* resolve __postFrameCallback crash on re-scheduling ([6a533ce](https://github.com/NativeScript/android/commit/6a533ce58b22b163d888c51f203be2ef3aa98347)) + + + +## [8.5.2](https://github.com/NativeScript/android/compare/v8.5.1...v8.5.2) (2023-08-31) + + +### Bug Fixes + +* __runOnMainThread erase iterator before it can be invalidated ([e811484](https://github.com/NativeScript/android/commit/e81148409046957fceb07b14394baa5bc055286d)) +* pull js stack trace from wrapped NativeScriptExceptions ([#1774](https://github.com/NativeScript/android/issues/1774)) ([52b7fa2](https://github.com/NativeScript/android/commit/52b7fa242cb4582701dcebf97aa0f6e0400bcb1a)) + + + +## [8.5.1](https://github.com/NativeScript/android/compare/v8.5.0...v8.5.1) (2023-07-24) + + +### Bug Fixes + +* Avoid setting the same property on an ObjectTemplate twice ([9e610c8](https://github.com/NativeScript/android/commit/9e610c8b61a721c04b0a7b8f5167f5b070c5419f)) +* Don't access `super` property on implementation object ([d8b8bc0](https://github.com/NativeScript/android/commit/d8b8bc02cf51de021855bb6a39ccd39ce1287304)) +* Don't iterate properties in GetImplementedInterfaces ([9dfae65](https://github.com/NativeScript/android/commit/9dfae6589caad31ee50a5946e7369f12505cd955)) +* intermediary fix for https://github.com/NativeScript/android/pull/1771 ([32c7abb](https://github.com/NativeScript/android/commit/32c7abb3418b20d1ac2d80c18c5e25a402937628)) +* Leave context after Runtime::PrepareV8Runtime() ([cd1d285](https://github.com/NativeScript/android/commit/cd1d2850e5c28d89339a90d783db1f1623957be7)) +* memory leak on accessing static interface methods ([88ce2d8](https://github.com/NativeScript/android/commit/88ce2d8b2c2ccf5568d1b95f0bf9df47179f658a)) +* memory leak on saving code cache ([6d416a1](https://github.com/NativeScript/android/commit/6d416a11e5b304d8ac2bfb4c48931a22e353df72)) +* Update common-runtime-tests-app ([c8db3ca](https://github.com/NativeScript/android/commit/c8db3cab6b3e3ddd41ab94adb8edac46a704cbe9)) +* update legacy android version in package.json ([#1744](https://github.com/NativeScript/android/issues/1744)) ([b4ad8e5](https://github.com/NativeScript/android/commit/b4ad8e541ff211055dc5197c7e0b350b6666771f)) +* Use Isolate::TryGetCurrent() ([afe026a](https://github.com/NativeScript/android/commit/afe026a29f3e9a1a5e533e4c66fd013f5f7f163c)) + + +* Remove weak callback from __postFrameCallback cache (#1755) ([ff1b979](https://github.com/NativeScript/android/commit/ff1b97975c7fdcd6b2acfb6153ba635d4f420eff)), closes [#1755](https://github.com/NativeScript/android/issues/1755) + + +### Features + +* add support for kotlin 1.8 ([#1765](https://github.com/NativeScript/android/issues/1765)) ([1a928e4](https://github.com/NativeScript/android/commit/1a928e47e733b1d4ba2bb83b4f541e1cac835155)) + + +### Performance Improvements + +* avoid unnecessary string copying when calling static properties of interfaces ([8b53d02](https://github.com/NativeScript/android/commit/8b53d022315d4a7fa9d617f431e0364030942d24)) + + +### BREAKING CHANGES + +* __startNDKProfiler() and __stopNDKProfiler() global +bindings no longer available. + +The NDK profiler was not functional, since nothing in the build process +defined the NDK_PROFILER_ENABLED preprocessor symbol. The start and stop +functions were already no-ops. + +* chore: Remove outdated comment + +RunMicrotasks no longer exists, it's already been replaced in the code +with PerformMicrotaskCheckpoint. + +* chore: Use unique_ptr for NewBackingStore in byte buffers + +V8 doc: https://docs.google.com/document/d/1sTc_jRL87Fu175Holm5SV0kajkseGl2r8ifGY76G35k/edit + +The V8 usage examples show unique_ptr here; it probably doesn't matter +much, but we don't need the backing store after creating the ArrayBuffer, +and I believe shared_ptr is slightly more overhead than unique_ptr. + +For convenience, replace the manual empty deleter for direct buffers with +v8::BackingStore::EmptyDeleter. + +* chore: Remove weak finalizer callback from __postFrameCallback() + +Weak finalizer callbacks are going away in V8 11.x, so we have to remove +this one. Luckily, a finalizer callback is not necessary - it's never +needed to prevent the frame callback from being collected. + +However, a weak callback is not necessary in the first place. We can just +clean up the cache entry after the callback is executed, since it is only +executed once. + +Note that the call to erase() destructs the FrameCallbackCacheEntry +instance, making `entry` a dangling pointer, so don't use it after the +erase(). There's probably a safer way to do this, although the way that +first occurred to me (pass the key to the callback instead of the entry, +and then use std::unordered_map::extract()) is not available on +robin_hood::unordered_map. + +* fix: Make sure frame callbacks are not ignored + +There was a bug where __postFrameCallback() would not always cause the +callback to be called. Without initializing `removed`, sometimes it would +have a nonzero value, so the callback would be ignored. + +* chore: Clear callback caches' persistent handles in destructor + +Clearing the persistent handles in the destructor makes it a bit easier to +deal with the cache entry's lifetime: they are cleared whenever the entry +is removed from the cache. + +We do this for both the main thread callback cache and the frame callback +cache. + +Adding a destructor makes the cache entries non-movable. But the only +place where they were moved was when inserting them into the cache anyway. +We can use C++17's try_emplace() method to insert them without having to +move them. + +* chore: Construct FrameCallbackCacheEntry with ID + +This avoids the situation of forgetting to add an ID to the cache entry. + +* chore: Improve usage of unordered_map APIs in CallbackHandlers + +This fixes a few places where we can avoid double lookups: + +- In RunOnMainThreadFdCallback, we already have a valid iterator, so no + need to look up the same key again in RemoveKey (this is the only usage + of RemoveKey, so we can remove it.) (Additionally, we probably want to + remove the cache entry before throwing a NativeScript exception.) + +- In PostFrameCallback and RemoveFrameCallback, we should not do + contains() immediately followed by find(). + +* chore: Fix runtime typo + +* chore: Ignore main thread and frame callback return values + +We don't do anything with the return value from these callbacks, so it's +OK to ignore them and not convert them to a local handle. + + + +# [8.5.0](https://github.com/NativeScript/android/compare/v8.4.0...v8.5.0) (2023-06-27) + + +### Bug Fixes + +* add semicolon after console type ([32259a9](https://github.com/NativeScript/android/commit/32259a90607bdd282d855c3701c7b7c2b203439d)) +* always log console messages and uncomment live sync ([c0f5514](https://github.com/NativeScript/android/commit/c0f5514686decc111d8b2e2f93c434141788f298)) +* Compile as C++17 ([221a9c2](https://github.com/NativeScript/android/commit/221a9c2c3747c2698238841ea56cf4d837206c20)) +* Correctly initialize context in inspector client init() ([0bc0480](https://github.com/NativeScript/android/commit/0bc0480035d01c681765a01b794b48db003be050)) +* drain microtasks after devtools message ([4834a2b](https://github.com/NativeScript/android/commit/4834a2b8e3d979323753d1da265ad1faaabbd655)) +* Implement console.log inspector with Runtime protocol ([2c4337b](https://github.com/NativeScript/android/commit/2c4337bf5b7c2c00bbcd5b34da1e8c08478fd37a)) +* memcpy array data for non-direct java bytebuffers ([#1740](https://github.com/NativeScript/android/issues/1740)) ([1c0214a](https://github.com/NativeScript/android/commit/1c0214a4785d425dc83f9612169fad7f70a28d41)) +* multi threading on MethodResolver ([bc8bc52](https://github.com/NativeScript/android/commit/bc8bc5253ff8abecf97bf98462d21fda15757a6a)) +* possible infinite loop and memory leak in metadata reader ([#1749](https://github.com/NativeScript/android/issues/1749)) ([c2c8aa8](https://github.com/NativeScript/android/commit/c2c8aa8a5f81c10aee293e14af797e5d1e3fbb4c)) +* Re-enable inspector code ([f357ce6](https://github.com/NativeScript/android/commit/f357ce6b9836be109ea5717ba0c09ee01a3f9876)) +* refactor console.log implementation a bit ([#1741](https://github.com/NativeScript/android/issues/1741)) ([d3c52cb](https://github.com/NativeScript/android/commit/d3c52cbaae8620d1ba1ed43d61da6df6f9df0f9c)) +* remove free of non-owned jni buffer ([81806b3](https://github.com/NativeScript/android/commit/81806b399f42a981a0cf54ab8a4be5712333e938)) +* Remove use of DISALLOW_COPY_AND_ASSIGN macro in inspector ([6da1a6b](https://github.com/NativeScript/android/commit/6da1a6bba0d9808568e8d6ae68fb0c71c6271ff1)) +* Remove use of V8InspectorPlatform ([1f2b202](https://github.com/NativeScript/android/commit/1f2b202263207de2045bafad5afedf786da788e1)) +* Restore DOM and Network callback handlers ([35689a7](https://github.com/NativeScript/android/commit/35689a7462ea86a760705c949dea6f4de4192cf8)) +* uncomment abifilters ([f5a5434](https://github.com/NativeScript/android/commit/f5a5434f41d764352de89133d2068b109bd3a54c)) +* update js arg conversions ([0640fce](https://github.com/NativeScript/android/commit/0640fcee3bd5429a637f3811fd7f8b4dc8ddf508)) +* use min sdk 17 ([2f2358c](https://github.com/NativeScript/android/commit/2f2358c0daf4c476110367432889740c18b49694)) + + +### Features + +* add console message type prefixes ([9a10e2b](https://github.com/NativeScript/android/commit/9a10e2b89410c6757bceba705a902060f37d97a1)) +* initial support for __inspectorSendEvent ([233b7c3](https://github.com/NativeScript/android/commit/233b7c39c2910e01adc261c4d0d3ddc1d92ef9d6)) +* native timer polyfills ([#1733](https://github.com/NativeScript/android/issues/1733)) ([3415e5c](https://github.com/NativeScript/android/commit/3415e5c515b3d73360a87f4f7aec03c44bd2c63c)) +* remove old WeakRef polyfill ([97d7465](https://github.com/NativeScript/android/commit/97d7465ca46df7240c4887fffb3167e469179a7a)) +* update all archs to use v8 10.3.22 ([a68b057](https://github.com/NativeScript/android/commit/a68b057da208338a83a620e64c0f4fecd90cff59)) +* use v8 static ([ee8a521](https://github.com/NativeScript/android/commit/ee8a5213a54d40046993ee61ac652842ccacfbe4)) + + +### Performance Improvements + +* refactor arrays marshaling ([84e7ddb](https://github.com/NativeScript/android/commit/84e7ddbe7fd503a3a5f11eed55246138068ca28f)) +* remove instantiations and cache runtime in isolate ([f23c1bb](https://github.com/NativeScript/android/commit/f23c1bb5b61c2fcaf7c6961450055bfd4fa4d385)) +* update old args converter with array marshaling optimizations and use this converter again tempporarily ([b5218c9](https://github.com/NativeScript/android/commit/b5218c9f5dce1e7c603887f4c9b65f3bd5284aa8)) +* use vector pointers for passing parsed method signatures ([aa623b7](https://github.com/NativeScript/android/commit/aa623b795ef38fb023cdef60d15641b8d8bf0338)) + + + +# [8.4.0](https://github.com/NativeScript/android/compare/v8.3.0...v8.4.0) (2022-11-30) + + +### Bug Fixes + +* don't assert for sub projects ([b5efdbc](https://github.com/NativeScript/android/commit/b5efdbc5ea11a7f5c73703ddae7485c4782bd8d7)) +* handle missing child classes when querying native classes ([#1718](https://github.com/NativeScript/android/issues/1718)) ([c238166](https://github.com/NativeScript/android/commit/c238166af3ce5205b65f2d79a23cfb963c3d60f9)) +* JvmField annotated fields ([#1726](https://github.com/NativeScript/android/issues/1726)) ([59da1cb](https://github.com/NativeScript/android/commit/59da1cbc8db888833fc24ea207b7d2f9195985b8)), closes [#1604](https://github.com/NativeScript/android/issues/1604) +* JvmName annotation & Kotlin building from App Resources ([1ba30be](https://github.com/NativeScript/android/commit/1ba30becfed7ba775412d722912a5e4db8c51e51)), closes [#1682](https://github.com/NativeScript/android/issues/1682) +* null data ([3b4a56d](https://github.com/NativeScript/android/commit/3b4a56df86f36a7738cc0089195d20239ce67273)) +* RunOnMainThreadCallback & PostFrameCallback ([#1721](https://github.com/NativeScript/android/issues/1721)) ([1ccd033](https://github.com/NativeScript/android/commit/1ccd033194d498b7614f16cc5d9cdcfb6a414f44)) +* typed array handling ([#1738](https://github.com/NativeScript/android/issues/1738)) ([00509ee](https://github.com/NativeScript/android/commit/00509eec6b929353a313530932f00757a3ccadb6)) +* typedarray & raf ([#1729](https://github.com/NativeScript/android/issues/1729)) ([796b9b1](https://github.com/NativeScript/android/commit/796b9b1fbba1e4cc81cb2098035ea38b3620a983)) + + +### Features + +* add class name to native objects ([#1723](https://github.com/NativeScript/android/issues/1723)) ([0c601ae](https://github.com/NativeScript/android/commit/0c601ae4680dcb6bea95f3292f6b0c3b9c1baeb1)) +* settings.gradle plugin ([#1731](https://github.com/NativeScript/android/issues/1731)) ([1759ecd](https://github.com/NativeScript/android/commit/1759ecd4071ccbeb60ea129af46e7f019c7523fe)) +* upgrade libzip to 1.9.2 ([#1724](https://github.com/NativeScript/android/issues/1724)) ([1dfd38f](https://github.com/NativeScript/android/commit/1dfd38ff0ea3a98b6d35f6f7c53f10ac1a788770)) + + + +# [8.3.0](https://github.com/NativeScript/android/compare/v8.2.4...v8.3.0) (2022-07-14) + + +### Bug Fixes + +* apply before-plugins before checking versions ([0df7362](https://github.com/NativeScript/android/commit/0df73625b72adb2fd654091f74e527dd61e3029b)) +* Set package version before preReleaseBuild ([12acd7f](https://github.com/NativeScript/android/commit/12acd7f3c1a265c0d8cddef8cb936825d2af7877)) + + +### Features + +* runOnMain, postFrameCallback & removeFrameCallback ([#1713](https://github.com/NativeScript/android/issues/1713)) ([bdd0313](https://github.com/NativeScript/android/commit/bdd031317c15d5e7eaa0dfeab2ef599b4eeddc4c)) + + + +## [8.2.3](https://github.com/NativeScript/android/compare/v8.2.2...v8.2.3) (2022-06-01) + + +### Bug Fixes + +* direct boot should not crash/ANR ([d2b18d7](https://github.com/NativeScript/android/commit/d2b18d7a8e23af6b27a8e2281e43bf72d715af66)) + + + +## [8.2.2](https://github.com/NativeScript/android/compare/v8.2.1...v8.2.2) (2022-03-09) + + +### Bug Fixes + +* app freezing on splash screen ([a61d3c1](https://github.com/NativeScript/android/commit/a61d3c1932ca11b67c88bc1b9a0eb97c9847ee19)) + + + +## [8.2.1](https://github.com/NativeScript/android/compare/v8.2.0...v8.2.1) (2022-03-08) + + +### Bug Fixes + +* add back gradle key in package json for the time being ([af925d5](https://github.com/NativeScript/android/commit/af925d50990820c9ca92a9a1a869518dbfd43fad)) + + + +# [8.2.0](https://github.com/NativeScript/android/compare/v7.0.1...v8.2.0) (2022-03-08) + + +### Bug Fixes + +* **chrome-devtools:** Elements tab ([e935a61](https://github.com/NativeScript/android/commit/e935a61de5043ae2febd76b34963bcaba7049791)), closes [#1641](https://github.com/NativeScript/android/issues/1641) [#1640](https://github.com/NativeScript/android/issues/1640) +* ensure buildMetadata is done before R8 ([01ad92e](https://github.com/NativeScript/android/commit/01ad92e53d6fcd6448b89004e1d49a66dd2b3254)) +* js -> buffer handling ([b49801c](https://github.com/NativeScript/android/commit/b49801cb7df3acd65ecb0a7e688afe2a25be908e)) +* load `before-plugins.gradle` in build.gradle to allow to override `androidBuildToolsVersion` and other vars ([d926dd4](https://github.com/NativeScript/android/commit/d926dd4340539ee9223ab9090ab20dd3c253af2a)) +* loop ([95ba9f5](https://github.com/NativeScript/android/commit/95ba9f5bd382cbffef2469fca632b69729c5837c)) +* material lib as debugImplementation ([c6946fe](https://github.com/NativeScript/android/commit/c6946fe6633ebb851fec35c19c2a64632628396e)) +* pending intent api31 flags ([d8781fd](https://github.com/NativeScript/android/commit/d8781fdcbffcf44d49d2df1055421eeecf8316ea)) +* update to jsparser ([ea1517c](https://github.com/NativeScript/android/commit/ea1517cbcd3b62037440c42fdcedf54eb2bee035)) +* **workers:** invalidate cached isolates on dispose ([#1704](https://github.com/NativeScript/android/issues/1704)) ([67e9daf](https://github.com/NativeScript/android/commit/67e9daf709722b608ef1fb6110d5b708f1819c41)) + + +### Features + +* commonGradleProperties ([77585ad](https://github.com/NativeScript/android/commit/77585ad58cceba74d8df69e5838b9f80dc947b49)) +* expose `PerformMicrotaskCheckpoint` ([4b58570](https://github.com/NativeScript/android/commit/4b58570fa3c43fc0a0ab73790030a414412f7c19)) +* log used androidX lib versions ([#1651](https://github.com/NativeScript/android/issues/1651)) ([528c46f](https://github.com/NativeScript/android/commit/528c46f99dd00859ba38c2671b7a5372d34f6a63)) +* make console.time use 3 decimals ([7d9f90b](https://github.com/NativeScript/android/commit/7d9f90bd6c45c9f1fd94904e6b17dd028fff1a11)) +* support passing typedArrays as nio buffers ([38230e5](https://github.com/NativeScript/android/commit/38230e5b282cf5ad04d3d655cf0671bc7e576a19)) +* working gradle7 ([501f884](https://github.com/NativeScript/android/commit/501f884120d6a884a4e2bb6d4d60312aa7efacff)) + + + +7.0.1 +== + +- [Gradle not respecting kotlinVersion (#1642)](https://github.com/NativeScript/android-runtime/pull/1642) +- [--debug-brk crashing android runtime (#1641)](https://github.com/NativeScript/android-runtime/pull/1641) + + +7.0.0 +== + +## Bug Fixes + +- [appPath & appResourcePath now passed in (#1635)](https://github.com/NativeScript/android-runtime/pull/1635) +- [Kotlin extension overwriting metadata & metadata duplication (#1633)](https://github.com/NativeScript/android-runtime/pull/1633) +- [Update gradle & kotlin dependancies (#1629)](https://github.com/NativeScript/android-runtime/pull/1629) + + +6.5.3 +== +- [Java 8 compatibility when build with Java 9 (#1625)](https://github.com/NativeScript/android-runtime/pull/1625) +- [metadata crash on startup with Local Notification plugin (#1621)](https://github.com/NativeScript/android-runtime/pull/1621) + + +6.5.1 +== + +## Bug Fixes + +- [Cleanup of warnings / possible not checked nullpointers (#1610)](https://github.com/NativeScript/android-runtime/pull/1610) + +- [[metadata] not rebuilding on json api usage change (#1589)](https://github.com/NativeScript/android-runtime/issues/1589) + + +6.5.0 +== + +### No changes + +6.4.1 +== + +## What's New + +- [[FR] Customize metadata generation to avoid adding unnecessary unused JS wrappers (#1485)](https://github.com/NativeScript/android-runtime/issues/1485) +- [Provide verbose metadata filtering output (#1583)](https://github.com/NativeScript/android-runtime/issues/1583) +- [Upgrade v8 to 8.0.426.16 (#1579)](https://github.com/NativeScript/android-runtime/issues/1579) +- [Upgrade android gradle plugin to the latest 3.5.3 version (#1564)](https://github.com/NativeScript/android-runtime/issues/1564) + +## Bug Fixes + +- [Using Android KTX is causing NativeScript app to crash (#1571)](https://github.com/NativeScript/android-runtime/issues/1571) +- [SBG generating conflicting class definitions (#1569)](https://github.com/NativeScript/android-runtime/issues/1569) +- [WebAssembly won't initialize without the debugger (#1558)](https://github.com/NativeScript/android-runtime/issues/1558) +- [Clean up allocations from finalized object links (#1566)](https://github.com/NativeScript/android-runtime/pull/1566) +- [Fix Kotlin Object issue (#1562)](https://github.com/NativeScript/android-runtime/pull/1562) + +6.3.1 +== + +## Bug Fixes + +- [Kotlin enum values not visible at runtime (#1560)](https://github.com/NativeScript/android-runtime/issues/1560) +- [Code cache breaks HMR (#1554)](https://github.com/NativeScript/android-runtime/issues/1554) +- [Worker memory leak in android (#1550)](https://github.com/NativeScript/android-runtime/issues/1550) + +6.3.0 +== + +## What's New + +- [Upgrade v8 to 7.8.279.19 (#1526)](https://github.com/NativeScript/android-runtime/issues/1526) +- [Restrict Kotlin internal modifier from metadata (#1551)](https://github.com/NativeScript/android-runtime/pull/1551) +- [Restrict Kotlin properties with non-public type (#1552)](https://github.com/NativeScript/android-runtime/issues/1552) + +## Bug Fixes + +- [Android 9: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in libNativeScript.so (#1413)](https://github.com/NativeScript/android-runtime/issues/1413) + +6.2.0 +== + +## What's New + - [Add Kotlin extension functions support (#1515)](https://github.com/NativeScript/android-runtime/issues/1515) + - [update dts generator to the latest version (#1506)](https://github.com/NativeScript/android-runtime/pull/1506) + - [Unify exceptions information in try/catch and __onUncaughtError (#1445)](https://github.com/NativeScript/android-runtime/issues/1445) + - [Interop between JS Objects and JSONObjects (#1500)](https://github.com/NativeScript/android-runtime/issues/1500) + - [Collect build analytics (#1501)](https://github.com/NativeScript/android-runtime/issues/1501) + - [expose the NDK revision used to build the V8 and the Runtime (#1498)](https://github.com/NativeScript/android-runtime/pull/1498) + - [Upgrade android gradle plugin to the latest 3.5.1 version (#1502)](https://github.com/NativeScript/android-runtime/issues/1502) + - [support snapshot libs out of the box (#1496)](https://github.com/NativeScript/android-runtime/pull/1496) + - [Upgrade v8 to 7.7.299.11 (#1478)](https://github.com/NativeScript/android-runtime/issues/1478) + +## Bug Fixes + - [Console.log in worker makes chrome debugging crash (#1511)](https://github.com/NativeScript/android-runtime/issues/1511) + - [fix searching for merge assets folder on windows (#1503)](https://github.com/NativeScript/android-runtime/pull/1503) + - [Background job with WorkManager (#1488)](https://github.com/NativeScript/android-runtime/issues/1488) + - [Deprecated API used in the ErrorActivity is crashing the app (when latest support library is used) (#1494)](https://github.com/NativeScript/android-runtime/issues/1494) + +6.1.2 +== + +## Bug Fixes + +- [UI freezes with tns-android 6.1.0 (Android only) (#1479)](https://github.com/NativeScript/android-runtime/issues/1479) + +6.1.1 +== + +## Bug Fixes + +- [When using kotlin sometimes the metadata is not existing in the result apk (#1476)](https://github.com/NativeScript/android-runtime/issues/1476) + + +6.1.0 +== + +## What's New + - [Runtime Binding Generator depends on deprecated APIs #1441)](https://github.com/NativeScript/android-runtime/issues/1441) + - [The runtime depends on accessing a hidden value field (#1458)](https://github.com/NativeScript/android-runtime/issues/1458) + - [Upgrade v8 to 7.6.303.28 (#1439)](https://github.com/NativeScript/android-runtime/issues/1439) + - [Unify JS stack trace when exception is thrown #1443](https://github.com/NativeScript/android-runtime/issues/1443) + - [Update androidSdk, targetSdk and build tools to 29 (#1452)](https://github.com/NativeScript/android-runtime/issues/1452) + - [Upgrade android gradle plugin to the latest 3.5.0 version (#1456)](https://github.com/NativeScript/android-runtime/issues/1456) + - [Add initial Kotlin support (#1459)](https://github.com/NativeScript/android-runtime/issues/1459) + - [Add support for user defined gradle.properties (#1463)](https://github.com/NativeScript/android-runtime/issues/1463) + + +## Bug Fixes + + - [SIGSEGV in libNativeScript.so on callback from java with console.log when displaying an object. #1366](https://github.com/NativeScript/android-runtime/issues/1366) + +6.0.2 +== + +## What's New + + - [Include x86_64 architecture](https://github.com/NativeScript/android-runtime/issues/1419) + +6.0.1 +== + +## Bug Fixes + +- [Arabic and Kurdish characters show as gibberish in console.log() (#1302)](https://github.com/NativeScript/android-runtime/issues/1302) +- [IntentService extending fails at runtime when service is started (#1426)](https://github.com/NativeScript/android-runtime/issues/1426) +- [SBG may fail when parsing big JS files (#1430)](https://github.com/NativeScript/android-runtime/issues/1430) +- [Upgrade android gradle plugin to the latest 3.4.2 version (#1425)](https://github.com/NativeScript/android-runtime/issues/1425) + + +6.0.0 +== + +## Breaking Changes + +- Exception information in onDiscarderError and onUnhandledError is changed so that `message` contains the exception message and `stackTrace` contains only the stackTrace. In the previous implementation `stackTrace` contained some additional details (including the exception message) and the `message` was something like: + + ``` + The application crashed because of an uncaught exception. You can look at "stackTrace" or "nativeException" for more detailed information about the exception. + ``` + +- [The built-in `JSON.stringify` method is used for cross workers communication](https://github.com/NativeScript/android-runtime/issues/1408). Circular object references are no longer supported and attempting to send such object will throw an exception. + + +## What's New + +- [Use the built-in JSON.stringify for cross workers communication (#1411)](https://github.com/NativeScript/android-runtime/pull/1411) +- [Enable AndroidX and Jetifier(#1370)](https://github.com/NativeScript/android-runtime/issues/1370) +- [Upgrade v8 to 7.5.288.22(#1387)](https://github.com/NativeScript/android-runtime/issues/1387) +- [Upgrade android gradle plugin to the latest 3.4.1 version(#1390)](https://github.com/NativeScript/android-runtime/issues/1390) +- [Remove printStackTrace method calls from the source code(#1359)](https://github.com/NativeScript/android-runtime/issues/1359) + +## Bug Fixes + +- [Improve package.json parsing in SBG (#1407)](https://github.com/NativeScript/android-runtime/pull/1407) +- [Improve error message in SBG class parsing (#1401)](https://github.com/NativeScript/android-runtime/pull/1401) +- [java.lang.NullPointerException in Metadata generator(#13795)](https://github.com/NativeScript/android-runtime/issues/1379) +- [Buffer() is deprecated(#1392)](https://github.com/NativeScript/android-runtime/pull/1392) +- [Warnings when building android(#1396)](https://github.com/NativeScript/android-runtime/issues/1396) +- [No JS stack on discardedError and unhandledError(#1354)](https://github.com/NativeScript/android-runtime/issues/1354) + + +5.4.0 +== + +## What's New + +- [Upgrade v8 to 7.4.288.25(#1356)](https://github.com/NativeScript/android-runtime/issues/1356) +- [Upgrade the android gradle plugin to the latest 3.4.0 version(#1360)](https://github.com/NativeScript/android-runtime/issues/1360) +- [Enable V8 symbols using from application's package.json file(#1368)](https://github.com/NativeScript/android-runtime/issues/1368) + +## Bug Fixes + +- [HashMaps improvements - fix for "Attempt to use cleared object reference"(#1345)](https://github.com/NativeScript/android-runtime/issues/1345) +- [Unable to start Service when the application process is killed by the OS(#1347)](https://github.com/NativeScript/android-runtime/pull/1347) +- [gradlew not compatible with sh(#1349)](https://github.com/NativeScript/android-runtime/issues/1349) +- [Memory leak in global.postMessage(#1358)](https://github.com/NativeScript/android-runtime/issues/1358) +- [memory leak java <-> javascript when java returns [] array(#1363)](https://github.com/NativeScript/android-runtime/issues/1363) +- [Bug when reifying some generic classes in the SBG(#1372)](https://github.com/NativeScript/android-runtime/issues/1372) +- [Extending an Android service fails if `onCreate` hasn't been overridden()](https://github.com/NativeScript/android-runtime/issues/1373) + +5.3.1 +== +## Bug Fixes + - [5.3 build failing (Android) (#1329)](https://github.com/NativeScript/android-runtime/issues/1329) + - [fix(build): Correct dependencies of `cleanupAllJars` gradle task (#1338)](https://github.com/NativeScript/android-runtime/pull/1338) + +5.3.0 +== + +## What's New + - [Upgrade v8 to 7.3.492.25(#1301)](https://github.com/NativeScript/android-runtime/issues/1301) + - [Upgrade the android gradle plugin to the latest 3.3.2 version(#1304)](https://github.com/NativeScript/android-runtime/issues/1304) + - [Fail SBG when there's no sbg-bindings.txt file generated(#1286)](https://github.com/NativeScript/android-runtime/issues/1286) + - [Enable arm64-v8 in app.gradle(#1284)](https://github.com/NativeScript/android-runtime/issues/1284) + - [Support external buildscript configurations(#1279)](https://github.com/NativeScript/android-runtime/issues/1279) + - [Refactor SBG to support generics and proper handling of overridable methods(#1322)](https://github.com/NativeScript/android-runtime/issues/1322) + +## Bug Fixes + - [The minSdk version should not be declared in the android manifest file(#1316)](https://github.com/NativeScript/android-runtime/issues/1316) + - ["Unable to resolve dependency" error when runtime is not build (#1309)](https://github.com/NativeScript/android-runtime/issues/1309) + - [App crashes after tns debug android --debug-brk and trying to debug with "Step into/over"(#892)](https://github.com/NativeScript/android-runtime/issues/892) + - [Unable to call plugin's native code if application has been build before adding the plugin(#1293)](https://github.com/NativeScript/android-runtime/issues/1293) + - [Android build fails when tns-core-modules is updated(#1257)](https://github.com/NativeScript/android-runtime/issues/1257) + - [Generate better code(#689)](https://github.com/NativeScript/android-runtime/issues/689) + +5.2.1 +== + +## Bug Fixes + - [Breakpoint stop to hit when you have two open tabs and close one(#1247)](https://github.com/NativeScript/android-runtime/issues/1247) + +5.2.0 +== + +## What's New + - [Upgrade v8 to 7.1.302.32(#1237)](https://github.com/NativeScript/android-runtime/issues/1237) + - [Add OnDiscardedError handler(#1245)](https://github.com/NativeScript/android-runtime/issues/1245) + - [Upgrade the android gradle plugin to the latest 3.3.1 version(#1251)](https://github.com/NativeScript/android-runtime/issues/1251) + - [Add android X support(#1226)](https://github.com/NativeScript/android-runtime/issues/1226) + - [Provide a JS helper function on the global object to release the native object wrapped by a JS instance(#1254)](https://github.com/NativeScript/android-runtime/issues/1254) + +## Bug Fixes + + - [ClassNotFound exception when calling nested static class with correct argument(#1195)](https://github.com/NativeScript/android-runtime/issues/1195) + - [If you refresh or close the chrome dev tools window an error will be log in the console (#1202)](https://github.com/NativeScript/android-runtime/issues/1202) + - [Debug on Android fails when stopped on breakpoint and change in .xml/.css/.html is applied(#1243)](https://github.com/NativeScript/android-runtime/issues/1243) + - [Upgrade V8 to v7 to fix unstable sort() method(#1176)](https://github.com/NativeScript/android-runtime/issues/1176) + - [CodeCache option is broken since Android Runtime 4.1.0(#1235)](https://github.com/NativeScript/android-runtime/issues/1235) + - [Snapshots with ABI splits do not work since Android Runtime 4.1.0(#1234)](https://github.com/NativeScript/android-runtime/issues/1234) + +5.1.0 +== + +## What's New + - [Add a setting to wrap calls to CallJSMethod in try catch(#1223)](https://github.com/NativeScript/android-runtime/issues/1223) + - [Support for abstract interface with static methods(#1157)](https://github.com/NativeScript/android-runtime/issues/1157) + - [Use gradle plugin 3.2.1 instead of 3.2.0(#1209)](https://github.com/NativeScript/android-runtime/pull/1209) + - [Add a concrete exception when the runtime cannot be found(#1201)](https://github.com/NativeScript/android-runtime/pull/1201) + +## Bug Fixes + + - [__extends not working as expected for non native inheritance(#1181)](https://github.com/NativeScript/android-runtime/issues/1181) + +5.0.0 +== + +## What's New + - [Upgrade v8 to 6.9.427.23(#1168)](https://github.com/NativeScript/android-runtime/issues/1168) + - [Added support for before-plugins.gradle file applied before plugin(#1183)](https://github.com/NativeScript/android-runtime/pull/1185) + - [Make JSParser in SBG fail the build when failing(#1152)](https://github.com/NativeScript/android-runtime/issues/1152) + - [Generate interface names list in SBG in parallel(#1132)](https://github.com/NativeScript/android-runtime/issues/1132) + - [Upgrade android gradle plugin to 3.2.0(#1147)](https://github.com/NativeScript/android-runtime/issues/1147) + +## Bug Fixes + + - [Static Binding Generator fails if class has static properties that are used within the class(#1160)](https://github.com/NativeScript/android-runtime/issues/1160) + - [Fixing NoClassDefFoundError when using older API(#1164)](https://github.com/NativeScript/android-runtime/pull/1164) + +4.2.0 +== + +## What's New + - [Upgrade v8 to 6.7.288.46(#1130)](https://github.com/NativeScript/android-runtime/issues/1130) + - [Static binding generator now uses bundled npm packages(#1096)](https://github.com/NativeScript/android-runtime/issues/1096) + - [Add gradle dependencies versions in package.json(#1102)](https://github.com/NativeScript/android-runtime/issues/1102) + - [Introduce a setting for auto catching exceptions when calling JS method native(#1119)](https://github.com/NativeScript/android-runtime/issues/1119) + - [Make livesync work entirely through named sockets(#932)](https://github.com/NativeScript/android-runtime/issues/932) + +## Bug Fixes + + - [Unable to increase minSdk version by modifying AndroidManifest.xml(#1104)](https://github.com/NativeScript/android-runtime/issues/1104) + - [UTF8 symbols in inspector protocol are not properly encoded(#1116)](https://github.com/NativeScript/android-runtime/issues/1116) + - [ChromeDevTools: If you close the socket the app will crash on the device/emulator(#1122)](https://github.com/NativeScript/android-runtime/issues/1122) + - [App tries to write in /data/local/tmp(#828)](https://github.com/NativeScript/android-runtime/issues/828) + - [Rewrite livesync implementation(#929)](https://github.com/NativeScript/android-runtime/pull/929) + +4.1.3 +== + +## Bug Fixes + + - Use google repository as primary gradle repository + +4.1.2 +== + +## Bug Fixes + + - [Webview Crash With Android 8.1.0 / Chromium 67.0.3396.68 (#1075)](https://github.com/NativeScript/android-runtime/issues/1075) + +4.1.1 +== + +## What's New + + - [Add support for java.nio.HeapByteBuffer to ArrayBuffer conversion(#1060)](https://github.com/NativeScript/android-runtime/issues/1060) + - [Upgrade Gradle version (wrapper and plugin) to latest(#1054)](https://github.com/NativeScript/android-runtime/issues/1054) + - [Accessing Native Packages starting with 'in'(#1046)](https://github.com/NativeScript/android-runtime/issues/1046) + - [tns-android 4.0.1: undefined objects are dumped with console.log making the log unreadable(#1026)](https://github.com/NativeScript/android-runtime/issues/1026) + - [Support new gradle dependency configuration(#993)](https://github.com/NativeScript/android-runtime/issues/993) + - [new Date() does not work as expected after time zone change(#961)](https://github.com/NativeScript/android-runtime/issues/961) + - [Update V8 to latest stable version(#808)](https://github.com/NativeScript/android-runtime/issues/808) + - [Checking if java class implements java interface(#739)](https://github.com/NativeScript/android-runtime/issues/739) + - [How to disable console.logs in release builds(#1024)](https://github.com/NativeScript/android-runtime/issues/1024) + +## Bug Fixes + + - [Calling java method with incorrect params crashes with a JNI exception (#844)](https://github.com/NativeScript/android-runtime/issues/844) + - [Chrome DevTools: App crashes after adding new view element(#1051)](https://github.com/NativeScript/android-runtime/issues/1051) + - [Extraneous classes generated by SBG are not cleaned on rebuild(#904)](https://github.com/NativeScript/android-runtime/issues/904) + - [Source files have different roots, while using debug command(896)](https://github.com/NativeScript/android-runtime/issues/896) + +4.0.1 +== + +## Bug Fixes + + - [App crash on startup on Android 4.4](https://github.com/NativeScript/android-runtime/issues/999) + +4.0.0 +== + +## Breaking Changes + + - [Rewrite the build script routine to no longer use flavors as the primary mechanism to apply nativescript plugin Android configurations (#890)](https://github.com/NativeScript/android-runtime/issues/890) - **If you see the `All flavors must now belong to a named flavor dimension.` build error, ensure that you are using the latest CLI. Plugins will be built implicitly by the CLI, version 4.0.0-rc or newer.** + - [Application package outputs are now located at `platforms/android/app/build/outputs///app-.apk` (#938)](https://github.com/NativeScript/android-runtime/issues/938) + +## What's New + + - [Respect `.jar` and `.aar` libraries from `App_Resources/Android/libs` (#899)](https://github.com/NativeScript/android-runtime/issues/899) + - [Add user-defined Android project files - `.java`, resources, assets in `App_Resources/Android` (#700)](https://github.com/NativeScript/android-runtime/issues/700) - **Enabled after updating the App_Resources/Android subdirectory structure. Use `tns resources update android` with CLI 4.0.0-rc or newer.** + - [Console API improvements - file name, line, column support in Chrome DevTools; Objects are expanded to JSON representations (#894) (#884)](https://github.com/NativeScript/android-runtime/pull/894) - **Replaces the console API in the `tns-core-modules`.** + - [Update Gradle to 4.1 and Android plugin for Gradle to 3.0.1 (#938)](https://github.com/NativeScript/android-runtime/issues/938) + +## Bug Fixes + + - [fix: static binding generator creating wrong files, when two extended classes have the same name (#692)](https://github.com/NativeScript/android-runtime/issues/692) + +3.4.2 +== + +## Bug Fixes + + - [fix include gradle flavor generation for plugins with incomplete include.gradle scripts (#937)](https://github.com/NativeScript/android-runtime/pull/937) + +3.4.1 +== + +## Bug Fixes + + - [Want help to access webview document height and cookies (#5243)](https://github.com/NativeScript/NativeScript/issues/5243) + - [Question about plugin using native lib NS 3.4 (#5254)](https://github.com/NativeScript/NativeScript/issues/5254) + +3.4.0 +== + +## What's New + + - [Android Studio Integration (#876)](https://github.com/NativeScript/android-runtime/issues/876) + +## Bug Fixes + + - [Faulty Java class name when extending a class inside a file containing dots (#761)](https://github.com/NativeScript/android-runtime/issues/761) + +3.3.1 +== + +## Bug Fixes + + - [app.gradle applies before other plugin gradle scripts (#878)](https://github.com/NativeScript/android-runtime/issues/878) + +3.3.0 +== + +## Bug Fixes + + - [Provide better error message while parsing js files (#833)](https://github.com/NativeScript/android-runtime/issues/833) + - [Improve JavaScript Metadata generation (#832)](https://github.com/NativeScript/android-runtime/issues/832) + - [Improve Error handling incorrectly implementing Java interface (#836)](https://github.com/NativeScript/android-runtime/issues/836) + +3.2.0 +== + +## Bug Fixes + + - [Static binding generator fails when using Webpack + Workers (#778)](https://github.com/NativeScript/android-runtime/issues/778) + +3.1.1 +== + +## Bug Fixes + - [APKS with ABI split crash on start up (#785)](https://github.com/NativeScript/android-runtime/issues/785) + +3.1.0 +== + +## What's New + - [Chrome DevTools Elements Tab Support (#746)](https://github.com/NativeScript/android-runtime/issues/746) + +3.0.1 +== + +## Bug Fixes + + - [--debug-brk flag not working (#2741)](https://github.com/NativeScript/nativescript-cli/issues/2741) + - [Clean app between different versions of application package. Ensure Android 6's AutoBackup feature doesn't restore files for NS apps](https://github.com/NativeScript/android-runtime/pull/771#issue-232247925) + +3.0.0 +== + +## What's New + + - [Chrome DevTools Network Domain (#715)](https://github.com/NativeScript/android-runtime/issues/715) + - [Chrome DevTools Scope Tab (#713)](https://github.com/NativeScript/android-runtime/issues/713) + - [Enabling java source code or direct dex generation #663)](https://github.com/NativeScript/android-runtime/issues/663) + - [Improve Gradle incremental build (#562)](https://github.com/NativeScript/android-runtime/issues/562) + + +## Bug Fixes + + - [Javascript array not marshalling to Java long[] properly (#696)](https://github.com/NativeScript/android-runtime/issues/696) + +2.5.0-RC +== + +## What's New + + - [Error when running on real android device (#628)](https://github.com/NativeScript/android-runtime/issues/628) + - [Updating v8 to 5.4/5.5 #631)](https://github.com/NativeScript/android-runtime/issues/631) + +## Bug Fixes + + - [Can't use npm packages ending with ".js" (#666)](https://github.com/NativeScript/android-runtime/issues/666) + - [Static binding generator crash build-time: clazz is null causes app to crash (#665)](https://github.com/NativeScript/android-runtime/issues/665) + - [Decorators aren't respected when extending classes with TypeScript 2.1.4+ (#651)](https://github.com/NativeScript/android-runtime/issues/651) + - [Samples SDK app crashes (#632)](https://github.com/NativeScript/android-runtime/issues/632) + - [Missing stack trace on worker errors (#629)](https://github.com/NativeScript/android-runtime/issues/629) + - [Classes using fields from compileSdk > Platform Sdk on device cause crash when extended in TS (#626)](https://github.com/NativeScript/android-runtime/issues/626) + - [A failure building in debug and release in series (#649)](https://github.com/NativeScript/android-runtime/issues/649) + +2.4.0 +== + +## What's New + + - [Update the V8 JavaScript Engine to 5.2.361 (97% ES6 support)](http://v8project.blogspot.bg/2016/06/release-52.html) + - [[Experimental] Generate typings for android.jar and android support libs (--androidTypings) (#605)](https://github.com/NativeScript/android-runtime/pull/605) + - [[Experimental] Multithreading support enabled with Web Workers API (#532)](https://github.com/NativeScript/android-runtime/issues/532) + - [Enable enableProguardInReleaseBuilds in build.gradle (#567)](https://github.com/NativeScript/android-runtime/issues/567) + - [Optimized default apk size (#529)](https://github.com/NativeScript/android-runtime/issues/529) + - [Improved debug Error Activity (#293)](https://github.com/NativeScript/android-runtime/issues/293) + +## Bug Fixes + + - [Build for armv7 and x86 only by default (#614)](https://github.com/NativeScript/android-runtime/issues/614) + - [Make javascript parsing during build incremental (#572)](https://github.com/NativeScript/android-runtime/issues/572) + - [App won't launch on Android 22 device (#592)](https://github.com/NativeScript/android-runtime/issues/592) + - [Metadata isn't created for generated dex files (#552)](https://github.com/NativeScript/android-runtime/issues/552) + - [App doesn't load with the new custom Application\Activity support (#546)](https://github.com/NativeScript/android-runtime/issues/546) + +## Performance + + - [Enable enableProguardInReleaseBuilds in build.gradle (#567)](https://github.com/NativeScript/android-runtime/issues/567) + - [Optimize default apk size (#529)](https://github.com/NativeScript/android-runtime/issues/529) + +2.3.0 +== + +## What's New + + - [Extend is not working as previous versions in all cases. (#514)](https://github.com/NativeScript/android-runtime/issues/514) + - [JS: Binding: Run-time error occured in file: undefined at line: undefined and column: undefined (#443)](https://github.com/NativeScript/android-runtime/issues/443) + +## Bug Fixes + + - [Crash when invoking a second (different) signature of an overloaded method (meta generator cache?) (#555)](https://github.com/NativeScript/android-runtime/issues/555) + - [App crashes when set undefined to reference type field (#306)](https://github.com/NativeScript/android-runtime/issues/306) + +2.2.0 +== + +## What's New + + - [Classes can implement multiple interfaces (#501)](https://github.com/NativeScript/android-runtime/pull/501) + +## Performance + + - [Update gradle wrapper version to 2.10, and gradle plugin to 2.1.2 (#516)](https://github.com/NativeScript/android-runtime/pull/516) + +2.1 +== + +## What's New + + - [Implement custom gradle clean (#459)](https://github.com/NativeScript/android-runtime/issues/459) + +## Bug Fixes + + - [App crash (#476)](https://github.com/NativeScript/android-runtime/issues/476) + - [The static binding generator should clean redundant files (#467)](https://github.com/NativeScript/android-runtime/issues/467) + - [Android builds fail on nativescript 2.0 (#460)](https://github.com/NativeScript/android-runtime/issues/460) + - [Need to add a Gradle Android.defaultConfig (#454)](https://github.com/NativeScript/android-runtime/issues/454) + +## Performance + + - [Initial builds of ng2 apps are slow (#436)](https://github.com/NativeScript/android-runtime/issues/436) + +2.0.0 +== + +## What's New + + - [Android N early developer preview (#378)](https://github.com/NativeScript/android-runtime/issues/378) + - [[Proposal] Static binding generator specification. (#363)](https://github.com/NativeScript/android-runtime/issues/363) + - [Android Runtime Support for older Android versions (#357)](https://github.com/NativeScript/android-runtime/issues/357) + - [Data Marshalling: Support for typed arrays (#65)](https://github.com/NativeScript/android-runtime/issues/65) + - [Support Android Widgets](https://github.com/NativeScript/android-runtime/issues/69) + - [Add support for caching already compiled JS code](https://github.com/NativeScript/android-runtime/issues/257) + - [Additional Intents Crashes app](https://github.com/NativeScript/android-runtime/issues/218) + - [Enable Multidex support](https://github.com/NativeScript/android-runtime/issues/344) + +## Bug Fixes + + - [Wrong object lifecycle management (#382)](https://github.com/NativeScript/android-runtime/issues/382) + - [CLI can easily fail and blow project up on windows when you have multiple plugins. (#369)](https://github.com/NativeScript/android-runtime/issues/369) + - [Provide method implementations for partially implemented interfaces. (#259)](https://github.com/NativeScript/android-runtime/issues/259) + - [Generate metadata for protected interfaces (#236)](https://github.com/NativeScript/android-runtime/issues/236) + - [Cannot resolve method/constructor signatures when null is passed (#90)](https://github.com/NativeScript/android-runtime/issues/90) + - [App seems to load up with a white screen on run, but works in debug mode #397](https://github.com/NativeScript/android-runtime/issues/397) + +## Performance + + - [Wrong object lifecycle management (#382)](https://github.com/NativeScript/android-runtime/issues/382) + +1.7.1 +== + +## Bug Fixes + + - [Fix application initialization](https://github.com/NativeScript/android-runtime/issues/396) + - [Fix error activity](https://github.com/NativeScript/android-runtime/issues/395) + +1.7.0 +== + +## What's New + - [Extendind Application and Activity classes](https://github.com/NativeScript/android-runtime/issues/226) + - Gradle script improvements + +## Bug Fixes + + - [App crashes with "NativeScriptApplication already initialized"](https://github.com/NativeScript/android-runtime/issues/362) + - [Upgrade to Gradle 1.5.0](https://github.com/NativeScript/android-runtime/issues/375) + + +1.6.0 +== + +## What's New + + - [Build common test infrastructure for Android and iOS](https://github.com/NativeScript/android-runtime/issues/68) + - [New syntax for Java arrays](https://github.com/NativeScript/android-runtime/issues/70) + - [Improved debugger](https://github.com/NativeScript/android-runtime/issues/112) + - [Log in the debugger console](https://github.com/NativeScript/android-runtime/issues/145) + - [Update documentation](https://github.com/NativeScript/android-runtime/issues/290) + - [Provide support for ARMv8a](https://github.com/NativeScript/android-runtime/issues/297) + - [Imroved exception handling](https://github.com/NativeScript/android-runtime/issues/300) + - Gradle script improvements + +## Bug Fixes + + - [Fix app crash during debugging (#270)](https://github.com/NativeScript/android-runtime/issues/270) + - [Fix app hang during array marshalling](https://github.com/NativeScript/android-runtime/issues/320) + - [Fix incorrect module resolution](https://github.com/NativeScript/android-runtime/issues/334) + - [Fix app crash during debugging (#338)](https://github.com/NativeScript/android-runtime/issues/338) + +1.5.1 +== + +## What's New + + - [Enable requiring of JSON files (like in Node) (#217)](https://github.com/NativeScript/android-runtime/issues/217) + - [Revisit the "assert" routine in the JNI part (#221)](https://github.com/NativeScript/android-runtime/issues/221) + - [Android CallStack (#228)](https://github.com/NativeScript/android-runtime/issues/228) + - [error handling introducing c++ exceptions (#277)](https://github.com/NativeScript/android-runtime/pull/277) + - [Simplify require errors (#287)](https://github.com/NativeScript/android-runtime/issues/287) + - [Experimental: Support native modules (#291)](https://github.com/NativeScript/android-runtime/issues/291) + - Gradle script improvements + +## Bug Fixes + + - [Print meaningful error when metadata generator fails to reflect a class (#245)](https://github.com/NativeScript/android-runtime/issues/245) + +1.5.0 +== + +## What's New + + - Improved LiveSync + - [Improved error handling](https://github.com/NativeScript/android-runtime/issues/221) + - Use Gradle Wrapper + - Use V8 code cache (experimental) + + ## Bug Fixes + + - Proper handling of HTTP 401 status code + - [Generate metadata for protected interfaces](https://github.com/NativeScript/android-runtime/issues/236) + - [Fix loading module with NULL char in it](https://github.com/NativeScript/android-runtime/issues/271) + +1.4.0 +== + +## Bug Fixes + + - [Generated metadata is not updated after initial build until after 'gradle clean' is called (#227)](https://github.com/NativeScript/android-runtime/issues/227) + - [Incorrect behavior when getting or setting java fields from javascript (#219)](https://github.com/NativeScript/android-runtime/issues/219) + - [Better handling of package.json main configuration (#190)](https://github.com/NativeScript/android-runtime/issues/190) + - [Calling non existen ctor crashesh the runtime (#180)](https://github.com/NativeScript/android-runtime/issues/180) + - [ClassCastException when tries to convert numeric return value of overridden methods (#139)](https://github.com/NativeScript/android-runtime/issues/139) + +1.3.0 +== + +## What's New + + - [Expose public API for NativeScript Companion App for deleting old *.dex files (#187)](https://github.com/NativeScript/android-runtime/issues/187) + - [Add support for AppBuilder LiveSync (#186)](https://github.com/NativeScript/android-runtime/issues/186) + - [Create a new template project for Gradle build (#182)](https://github.com/NativeScript/android-runtime/issues/182) + - [Support ~ path syntax in require (#177)](https://github.com/NativeScript/android-runtime/issues/177) + - [Enable using the Google Design library (and alike) with "library add" command (#140)](https://github.com/NativeScript/android-runtime/issues/140) + - [Implement support for CLI live sync feature (#137)](https://github.com/NativeScript/android-runtime/issues/137) + - [Ahead-of-time generation of binding proxies (#103)](https://github.com/NativeScript/android-runtime/issues/103) + - [Use pool of arrays for marshalling (#33)](https://github.com/NativeScript/android-runtime/issues/33) + +## Bug Fixes + + - [Run after LiveSync starts the last synced app on the device/emulator (#214)](https://github.com/NativeScript/android-runtime/issues/214) + - [Cannot load module with relative path on Android 6 (#206)](https://github.com/NativeScript/android-runtime/issues/206) + - [App crashes when call overloaded method of a base class (#203)](https://github.com/NativeScript/android-runtime/issues/203) + - [Fix file is external to application error on Android M (#185)](https://github.com/NativeScript/android-runtime/issues/185) + - [App crash during GC (#184)](https://github.com/NativeScript/android-runtime/issues/184) + - [JNI reference leaks when passing JavaScript arrays (#167)](https://github.com/NativeScript/android-runtime/issues/167) + - [ArrayBuffer broken (#164)](https://github.com/NativeScript/android-runtime/issues/164) + - [ClassCastException when tries to convert numeric return value of overridden methods (#139)](https://github.com/NativeScript/android-runtime/issues/139) + - [Allow debugger reconnects (#136)](https://github.com/NativeScript/android-runtime/issues/136) + - [__onUncaughtError is not called. (#108)](https://github.com/NativeScript/android-runtime/issues/108) + +## Performance + + - [Cache parsed method signature (#181)](https://github.com/NativeScript/android-runtime/issues/181) + - [Use pool of arrays for marshalling (#33)](https://github.com/NativeScript/android-runtime/issues/33) + +# Android Runtime Changelog + +1.2.1 +== + +## What's New + + - Updated android widgets library + - [Allow verbose logging system property to enable debug messages early in engine bootstrap](https://github.com/NativeScript/android-runtime/issues/111) + +## Bug Fixes + + - [Additional null checks in V8 to handle certain possible garbage collection issues](https://github.com/NativeScript/android-runtime/issues/111) + - Fix sync support in runtime for specific (Samsung) Android devices where run-as is not working + - [Fix JNI memory leak](https://github.com/NativeScript/android-runtime/issues/167) + - Fix copy of correct android.jar referenced in project properties + + +1.2.0 +== + +## What's New + + - [Support http cookies in build-in http client (#159)](https://github.com/NativeScript/android-runtime/issues/159) + - [Implement support for CLI live sync feature (#137)](https://github.com/NativeScript/android-runtime/issues/137) + - [Rethink 0 day support for new android versions (#86)](https://github.com/NativeScript/android-runtime/issues/86) + +## Bug Fixes + + - [Fix ErrorActivity not displayed on uncaught exceptions (#158)](https://github.com/NativeScript/android-runtime/issues/158) + - [Fix JNI memory leak when resolving classes in metadata reader (#157)](https://github.com/NativeScript/android-runtime/issues/157) + - [Application crash with JNI ERROR (app bug): local reference table overflow (max=512) (#149)](https://github.com/NativeScript/android-runtime/issues/149) + - [Cannot set float field (#148)](https://github.com/NativeScript/android-runtime/issues/148) + - [IndexedPropertySetter not called (#127)](https://github.com/NativeScript/android-runtime/issues/127) + - [Fix GetDbgPort intent (#117)](https://github.com/NativeScript/android-runtime/issues/117) + - [Recreating an Activity with fragments on same process crashes the runtime (#96)](https://github.com/NativeScript/android-runtime/issues/96) + - [Grunt tasks fail on Windows (#61)](https://github.com/NativeScript/android-runtime/issues/61) + +## Performance + + - [Optimize Strings marshaling between Java and V8 (#160)](https://github.com/NativeScript/android-runtime/issues/160) + - [Improve required module loading (#156)](https://github.com/NativeScript/android-runtime/issues/156) + - [Improve JNI String marshalling (#126)](https://github.com/NativeScript/android-runtime/issues/126) + - [Cache folder-as-module resolved path (#121)](https://github.com/NativeScript/android-runtime/issues/121) + + +## 1.1.0 (2015, June 10) + +### New + +* Implemented [#58](https://github.com/NativeScript/android-runtime/issues/60) to remove the 3-seconds initial timeout for Debug builds. +* Implemented [#118](https://github.com/NativeScript/android-runtime/issues/118) to replace the MultiDex library with DexClassLoader. +* Started [#103](https://github.com/NativeScript/android-runtime/issues/103) AOT proxy generation to improve startup time and to enable new scenarios like BroadcastReceivers, BackgroundServices and arbitrary Activity types declared in the manifest. + +### Fixed + +* [#63](https://github.com/NativeScript/android-runtime/issues/63). An issue which prevented users to extend overloaded methods. +* [#64](https://github.com/NativeScript/android-runtime/issues/64). A JNI Crash when calling JS method with char. +* [#113](https://github.com/NativeScript/android-runtime/issues/113). Fixes the extend routine for an Activity. +* [#114](https://github.com/NativeScript/android-runtime/issues/114). Removes the redundant setNativeScriptOverrides method. + +## 1.0.0 (2015, April 29) + +### New + +* Updated the V8 version to 4.1.0.27. +* Re-implemented debugger support (no more spontaneous dead locks). + +### Fixed + +* An issue with the error reporting routine. + +### Breaking Changes + +* Renamed global functions: + * `__log` (was `Log`) + * `__debugbreak` (was `waitForDebugger`) + * `__enableVerboseLogging` (was `enableVerboseLogging`) + * `__disableVerboseLogging` (was `disableVerboseLogging`) + * `__exit` (was `fail`) + +## 0.10.0 (2015, April 17) + +### New + +* Added Dynamic Generator for binding proxies. This boosts the initial loading time, especially on Android 5.0+ devices. +* Added several optimization techniques, which further optimize the loading time and the overall performance. +* Improved the error reporting mechanism for Debug builds. +* Added support for package.json and index.js for bootstrapping an application. + +### Breaking Changes + + * Removed the simulated property-like support for Android types. E.g. the `android.content.Intent.getAction()` previously was accessible like `android.content.Intent.Action`. This is no longer valid as it contradicts with the Android APIs. + * Changed the way `extend` constructs work +```javascript +// WRONG +var handler = new android.os.Handler.extend({...})(); + +// CORRECT +var handlerType = android.os.Handler.extend({...}); +var handler = new handlerType(); +``` + * The directory structure in the `assets` folder has changed. The `tns_modules` directory is now within the `assets/app` one. To migrate older CLI projects to the new structure simply move the content of the inner app folder one level up: + +####Previous structure: +``` +|--app +|--|--app +|--|--|--bootstrap.js +|--|--|--myFile.js +|--|--tns_modules +``` + +####New structure: +``` +|--app +|--|--bootstrap.js +|--|--myFile.js +|--|--tns_modules +``` diff --git a/platforms/android/CODE_OF_CONDUCT.md b/platforms/android/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..e190410a8 --- /dev/null +++ b/platforms/android/CODE_OF_CONDUCT.md @@ -0,0 +1,81 @@ +# NativeScript Community Code of Conduct + +Our community members come from all walks of life and are all at different stages of their personal and professional journeys. To support everyone, we've prepared a short code of conduct. Our mission is best served in an environment that is friendly, safe, and accepting; free from intimidation or harassment. + +Towards this end, certain behaviors and practices will not be tolerated. + +## tl;dr + +- Be respectful. +- We're here to help. +- Abusive behavior is never tolerated. +- Violations of this code may result in swift and permanent expulsion from the NativeScript community channels. + +## Contact + +- support@nativescript.org + +## Scope + +We expect all members of the NativeScript community, including administrators, users, facilitators, and vendors to abide by this Code of Conduct at all times in our community venues, online and in person, and in one-on-one communications pertaining to NativeScript affairs. + +This policy covers the usage of the NativeScript Slack community, as well as the NativeScript support forums, NativeScript GitHub repositories, the NativeScript website, and any NativeScript-related events. This Code of Conduct is in addition to, and does not in any way nullify or invalidate, any other terms or conditions related to use of NativeScript. + +The definitions of various subjective terms such as "discriminatory", "hateful", or "confusing" will be decided at the sole discretion of the NativeScript administrators. + +## Friendly, Harassment-Free Space + +We are committed to providing a friendly, safe, and welcoming environment for all, regardless of gender identity, sexual orientation, disability, ethnicity, religion, age, physical appearance, body size, race, or similar personal characteristics. + +We ask that you please respect that people have differences of opinion regarding technical choices, and acknowledge that every design or implementation choice carries a trade-off and numerous costs. There is seldom a single right answer. A difference of technology preferences is never a license to be rude. + +Any spamming, trolling, flaming, baiting, or other attention-stealing behaviour is not welcome, and will not be tolerated. + +Harassing other users of NativeScript is never tolerated, whether via public or private media. + +Avoid using offensive or harassing package names, nicknames, or other identifiers that might detract from a friendly, safe, and welcoming environment for all. + +Harassment includes, but is not limited to: harmful or prejudicial verbal or written comments related to gender identity, sexual orientation, disability, ethnicity, religion, age, physical appearance, body size, race, or similar personal characteristics; inappropriate use of nudity, sexual images, and/or sexually explicit language in public spaces; threats of physical or non-physical harm; deliberate intimidation, stalking or following; harassing photography or recording; sustained disruption of talks or other events; inappropriate physical contact; and unwelcome sexual attention. + +## Acceptable Content + +The NativeScript administrators reserve the right to make judgement calls about what is and isn't appropriate in published content. These are guidelines to help you be successful in our community. + +Content must contain something applicable to the previously stated goals of the NativeScript community. "Spamming", that is, publishing any form of content that is not applicable, is not allowed. + +Content must not contain illegal or infringing content. You should only publish content to NativeScript properties if you have the right to do so. This includes complying with all software license agreements or other intellectual property restrictions. For example, redistributing an MIT-licensed module with the copyright notice removed, would not be allowed. You will be responsible for any violation of laws or others’ intellectual property rights. + +Content must not be malware. For example, content (code, video, pictures, words, etc.) which is designed to maliciously exploit or damage computer systems, is not allowed. + +Content name, description, and other visible metadata must not include abusive, inappropriate, or harassing content. + +## Reporting Violations of this Code of Conduct + +If you believe someone is harassing you or has otherwise violated this Code of Conduct, please contact the administrators and send us an abuse report. If this is the initial report of a problem, please include as much detail as possible. It is easiest for us to address issues when we have more context. + +## Consequences + +All content published to the NativeScript community channels is hosted at the sole discretion of the NativeScript administrators. + +Unacceptable behavior from any community member, including sponsors, employees, customers, or others with decision-making authority, will not be tolerated. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. + +If a community member engages in unacceptable behavior, the NativeScript administrators may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event or service). + +## Addressing Grievances + +If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify the administrators. We will do our best to ensure that your grievance is handled appropriately. + +In general, we will choose the course of action that we judge as being most in the interest of fostering a safe and friendly community. + +## Contact Info +Please contact Dan Wilson @DanWilson if you need to report a problem or address a grievance related to an abuse report. + +You are also encouraged to contact us if you are curious about something that might be "on the line" between appropriate and inappropriate content. We are happy to provide guidance to help you be a successful part of our community. + +## Credit and License + +This Code of Conduct borrows heavily from the WADE Code of Conduct, which is derived from the NodeBots Code of Conduct, which in turn borrows from the npm Code of Conduct, which was derived from the Stumptown Syndicate Citizen's Code of Conduct, and the Rust Project Code of Conduct. + +This document may be reused under a Creative Commons Attribution-ShareAlike License. \ No newline at end of file diff --git a/platforms/android/CONTRIBUTING.md b/platforms/android/CONTRIBUTING.md new file mode 100644 index 000000000..4d5801b1d --- /dev/null +++ b/platforms/android/CONTRIBUTING.md @@ -0,0 +1,139 @@ +# Contributing to NativeScript + +## Before You Start + +Anyone wishing to contribute to the NativeScript project MUST read & sign the [NativeScript Contribution License Agreement](http://www.nativescript.org/cla). The NativeScript team cannot accept pull requests from users who have not signed the CLA first. + +## Introduction + +These guidelines are here to facilitate your contribution and streamline the process of getting changes merged into this project and released. Any contributions you can make will help tremendously, even if only in the form of an issue report. Following these guidelines will help to streamline the pull request and change submission process. + +## Reporting Bugs + +1. Always update to the most recent master release; the bug may already be resolved. +2. Search for similar issues in the issues list for this repo; it may already be an identified problem. +3. If this is a bug or problem that is clear, simple, and is unlikely to require any discussion -- it is OK to open an issue on GitHub with a reproduction of the bug including workflows and screenshots. If possible, submit a Pull Request with a failing test, entire application or module. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Code Fixes and Enhancements" section). + +## Requesting New Features + +1. Use Github Issues to submit feature requests. +2. First search for a similar request and extend it if applicable. This way it would be easier for the community to track the features. +2. When a brand new feature requested, try to give as many details on your need as possible. We prefer that you explain a need than explain a technical solution for it. That might trigger a nice conversation on finding the best and broadest technical solution to a specific need. + +## Asking for Help + +-------------The NativeScript team does *not* provide guaranteed formal support, except to those customers who have purchased a [commercial license for AppBuilder](http://www.telerik.com/platform) (Professional, Enterprise, etc.) or a support-only package from Telerik.com. Please do not create support requests for this project in the issues list for this repo, as these will be immediately closed and you'll be directed to post your question on a community forum. + +## Code Fixes and Enhancements + +### 1. Log an Issue + +Before doing anything else, we ask that you file an issue in the Issues list for this project. First, be sure to check the list to ensure that your issue hasn't already been logged. If you're free and clear, file an issue and provide a detailed description of the bug or feature you're interested in. If you're also planning to work on the issue you're creating, let us know so that we can help and provide feedback. To help us investigate your issue and respond in a timely manner, you can provide is with the following details: +- **Overview of the issue**: Provide a short description of the visible symptoms. If applicable, include error messages, screen shots, and stack traces. +- **Motivation for or use case**: Let us know how this particular issue affects your work. +- **Telerik NativeScript version(s)**: List the current version and build number of the CLI interface, the runtime and the modules. You can get these by checking the package.json files of the respective package. Let us know if you have not observed this behavior in earlier versions and if you consider it a regression. +- **System configuration**: Provide us with relevant system configuration information such as operating system, network connection, proxy usage, etc. Let us know if you have been able to reproduce the issue on multiple setups. +- **Steps to reproduce**: If applicable, submit a step-by-step walkthrough of how to reproduce the issue. +- **Related issues**: If you discover a similar issue in our archive, give us a heads up - it might help us identify the culprit. +- **Suggest a fix**: You are welcome to suggest a bug fix or pinpoint the line of code or the commit that you believe has introduced the issue. + +### 2. Fork and Branch + +#### Fork Us, Then Create A Topic Branch For Your Work + +The work you are doing for your pull request should not be done in the master branch of your forked repository. Create a topic branch for your work. This allows you to isolate the work you are doing from other changes that may be happening. + +Github is a smart system, too. If you submit a pull request from a topic branch and we ask you to fix something, pushing a change to your topic branch will automatically update the pull request. + +#### Isolate Your Changes For The Pull Request + +See the previous item on creating a topic branch. + +If you don't use a topic branch, we may ask you to re-do your pull request on a topic branch. If your pull request contains commits or other changes that are not related to the pull request, we will ask you to re-do your pull request. + +#### Branch from "master" + +The "master" branch of the NativeScript repository is for continuous contribution. Always create a branch for your work from the "master" branch. This will facilitate easier pull request management. + +#### Contribute to the Code Base +Before you submit a Pull Request, consider the following guidelines. + +Search GitHub for an open or closed Pull Request that relates to your submission. +Clone the repository. +```bash + git clone git@github.com:NativeScript/android-runtime.git +``` +Make your changes in a new git branch. We use the Gitflow branching model so you will have to branch from our master branch. +```bash + git checkout -b my-fix-branch master +``` +Create your patch and include appropriate test cases. +Build your changes locally. +```bash + grunt +``` +Commit your changes and create a descriptive commit message (the commit message is used to generate release notes). +```bash + git commit -a +``` +Push your branch to GitHub. +```bash + git push origin my-fix-branch +``` +In GitHub, send a Pull Request to NativeScript:android-runtime:master. +If we suggest changes, you can modify your branch, rebase, and force a new push to your GitHub repository to update the Pull Request. +```bash + git rebase master -i + git push -f +``` +That's it! Thank you for your contribution! + +When the patch is reviewed and merged, you can safely delete your branch and pull the changes from the main (upstream) repository. + +Delete the remote branch on GitHub. +```bash + git push origin --delete my-fix-branch +``` +Check out the master branch. +```bash + git checkout master -f +``` +Delete the local branch. +```bash + git branch -D my-fix-branch +``` +Update your master branch with the latest upstream version. +```bash + git pull --ff upstream master +``` + +#### Squash your commits + +When you've completed your work on a topic branch, we prefer that you squash your work down into a single commit to make the merge process easier. For information on squashing via an interactive rebase, see [the rebase documentation on GitHub](https://help.github.com/articles/interactive-rebase) + +### 3. Submit a Pull Request + +See [Github's documentation for pull requests](https://help.github.com/articles/using-pull-requests). + +Pull requests are the preferred way to contribute to NativeScript. Any time you can send us a pull request with the changes that you want, we will have an easier time seeing what you are trying to do. But a pull request in itself is not usually sufficient. There needs to be some context and purpose with it, and it should be done against specific branch. + +### Provide A Meaningful Description + +Similar to reporting an issue, it is very important to provide a meaningful description with your pull requests that alter any code. A good format for these descriptions will include four things: + +1. Why: The problem you are facing (in as much detail as is necessary to describe the problem to someone who doesn't know anything about the system you're building) + +2. What: A summary of the proposed solution + +3. How: A description of how this solution solves the problem, in more detail than item #2 + +4. Any additional discussion on possible problems this might introduce, questions that you have related to the changes, etc. + +Without at least the first 2 items in this list, we won't have any clue why you're changing the code. The first thing we'll ask, then, is that you add that information. + +## Code Style + +We are currently using Eclipse to write code. We are using predefined code formating rules for C++ and Java: + +* [C++](./CodingStyle.Cpp.xml) +* [Java](./CodingStyle.Java.xml) diff --git a/platforms/android/CodingStyle.Cpp.xml b/platforms/android/CodingStyle.Cpp.xml new file mode 100644 index 000000000..fa55f9824 --- /dev/null +++ b/platforms/android/CodingStyle.Cpp.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platforms/android/CodingStyle.Java.xml b/platforms/android/CodingStyle.Java.xml new file mode 100644 index 000000000..6e0862dbe --- /dev/null +++ b/platforms/android/CodingStyle.Java.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platforms/android/LICENSE b/platforms/android/LICENSE new file mode 100755 index 000000000..451412229 --- /dev/null +++ b/platforms/android/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2020, nStudio, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/platforms/android/README.md b/platforms/android/README.md new file mode 100644 index 000000000..4a2b4db02 --- /dev/null +++ b/platforms/android/README.md @@ -0,0 +1,121 @@ +

+ NativeScript Logo +

+ +

Node-API Android Runtime for NativeScript

+ +
+ +![NPM Version (with dist tag)](https://img.shields.io/npm/v/%40nativescript%2Fandroid/napi-v8?style=for-the-badge&logo=nativescript) +![NPM Version (with dist tag)](https://img.shields.io/npm/v/%40nativescript%2Fandroid/napi-quickjs?style=for-the-badge&logo=nativescript) +![NPM Version (with dist tag)](https://img.shields.io/npm/v/%40nativescript%2Fandroid/napi-hermes?style=for-the-badge&logo=nativescript) +![NPM Version (with dist tag)](https://img.shields.io/npm/v/%40nativescript%2Fandroid/napi-jsc?style=for-the-badge&logo=nativescript) + +
+ +[NativeScript](https://www.nativescript.org/) is an open-source framework for building truly native mobile applications using JavaScript. This repository contains the source code for the Node-API based Android Runtime used by NativeScript. + +The Android Runtime is a key component of the NativeScript framework. It is responsible for executing JavaScript code on Android devices. The runtime is built on top of [Node-API](https://nodejs.org/api/n-api.html) and provides a way to interact with the Android platform APIs from JavaScript. + +The new runtime is based on the Node-API and is designed to be more stable, faster, and easier to maintain. It also supports multiple JavaScript engines, including [V8](https://v8.dev/), [QuickJS](https://github.com/quickjs-ng/quickjs/), [Hermes](https://github.com/facebook/hermes), and [JavaScriptCore](https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html). + + + +- [Main Projects](#main-projects) +- [Helper Projects](#helper-projects) +- [Build Prerequisites](#build-prerequisites) +- [How to build](#how-to-build) +- [How to run tests](#how-to-run-tests) +- [Misc](#misc) +- [Get Help](#get-help) + + + +## Project structure + +The repo is structured in the following projects (ordered by dependencies): + +- [**android-metadata-generator**](android-metadata-generator) - generates metadata necessary for the Android Runtime. +- [**android-binding-generator**](test-app/runtime-binding-generator) - enables Java & Android types to be dynamically created at runtime. Needed by the `extend` routine. +- [**android-runtime**](test-app/runtime) - contains the core logic behind the NativeScript's Android Runtime. This project contains native C++ code and needs the Android NDK to build properly. +- [**android-runtime-testapp**](test-app/app) - this is a vanilla Android Application, which contains the tests for the runtime project. +- [**napi-implementations**](test-app/runtime/src/main//cpp/napi/) - contains the implementation of the Node-API for each supported JS engine. + +## Helper Projects + +- [**android-static-binding-generator**](android-static-binding-generator) - build tool that generates bindings based on the user's javascript code. +- [**project-template**](build-artifacts/project-template-gradle) - this is an empty placeholder Android Application project, used by the [NativeScript CLI](https://github.com/NativeScript/nativescript-cli) when building an Android project. + +### Build Prerequisites + +Following are the minimal prerequisites to build the runtime package. + +- Install the latest [Android Studio](https://developer.android.com/studio/index.html). +- From the SDK Manager (Android Studio -> Tools -> Android -> SDK Manager) install the following components: + + - Android API Level 23, 24, 25, 26, 27 + - Android NDK + - Android Support Repository + - Download Build Tools + - CMake + - LLDB + +## How to Build + +Clone the repo: + +```Shell +git clone https://github.com/NativeScript/napi-android.git +``` + +Set the following environment variables: + +- `JAVA_HOME` such that `$JAVA_HOME/bin/java` points to your Java executable +- `ANDROID_HOME` pointing to where you have installed the Android SDK +- `ANDROID_NDK_HOME` pointing to the version of the Android NDK needed for this version of NativeScript + +Run the following commands to build the runtime: + +``` +npm run setup +``` + +``` +npm run build +``` + +The command will let you interactively choose the JS engine you want to build with. i.e V8, QuickJS, Hermes or JSC, PrimJS, Static Hermes. + +- The build process includes building of the runtime package (both optimized and with unstripped v8 symbol table), as well as all supplementary tools used for the android builds: metadata-generator, binding-generator, metadata-generator, static-binding-generator +- The result of the build will be in the dist\_[engine] folder. For example if you are building with V8, the result will be in the dist_v8 folder. + +## How to Run Tests + +- Go to subfolder test-app after you built the runtime. +- Start an emulator or connect a device. + +- Run command + +```Shell +gradlew runtests +``` + +## Working with the Runtime in Android Studio + +- Open the test-app folder in Android Studio. It represents a valid Android project and you are able to build and run a test application working with the Runtime from the source. + +Note: You might need to run the Android Studio from the command line in order to preserve the environment variables. This is in case you get errors like "missing npm" if starting the studio the usual way. + +You can change the JS engine used by the runtime by setting the `jsEngine` property in the [`build.gradle`](test-app/runtime/build.gradle) file in the root of the project. The possible values are `QUICKJS`, `HERMES`, `JSC` or `V8`. + +## Contribute + +We love PRs! Check out the [contributing guidelines](CONTRIBUTING.md). If you want to contribute, but you are not sure where to start - look for [issues labeled `help wanted`](https://github.com/NativeScript/napi-android/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). + +## Get Help + +Please, use [github issues](https://github.com/NativeScript/napi-android/issues) strictly for [reporting bugs](CONTRIBUTING.md#reporting-bugs) or [requesting features](CONTRIBUTING.md#requesting-new-features). For general questions and support, check out [Stack Overflow](https://stackoverflow.com/questions/tagged/nativescript) or ask our experts in [NativeScript community on Discord](https://nativescript.org/discord). + +## License + +This project is licensed under the Apache License Version 2.0. See the [LICENSE file](LICENSE) for more info. diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/AndroidManifest.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..087db9a59 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/java/com/tns/NativeScriptApplication.java b/platforms/android/build-artifacts/project-template-gradle/app/src/main/java/com/tns/NativeScriptApplication.java new file mode 100644 index 000000000..83353eb7a --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/java/com/tns/NativeScriptApplication.java @@ -0,0 +1,39 @@ +package com.tns; + +import android.app.Application; +import android.os.Build; +import androidx.multidex.MultiDex; + +public class NativeScriptApplication extends Application { + + private static NativeScriptApplication thiz; + + public NativeScriptApplication() { + thiz = this; + } + + public void onCreate() { + ManualInstrumentation.Frame frame = ManualInstrumentation.start("NativeScriptApplication.onCreate"); + try { + super.onCreate(); + com.tns.Runtime runtime = RuntimeHelper.initRuntime(this); + if (runtime != null) { + runtime.run(); + } + } finally { + frame.close(); + } + } + + public void attachBaseContext(android.content.Context base) { + super.attachBaseContext(base); + if (Build.VERSION.SDK_INT < 21) { + // As the new gradle plugin automatically uses multidex if necessary we need to call this for older android API versions + MultiDex.install(this); + } + } + + public static Application getInstance() { + return thiz; + } +} diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/colors.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/colors.xml new file mode 100644 index 000000000..1fbdcf647 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/colors.xml @@ -0,0 +1,4 @@ + + + #65adf1 + \ No newline at end of file diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/styles.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/styles.xml new file mode 100644 index 000000000..da2272ea5 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/colors.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..87f11778d --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #F5F5F5 + #757575 + #65adf1 + \ No newline at end of file diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/errorstyles.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/errorstyles.xml new file mode 100644 index 000000000..f16d76814 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/errorstyles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/strings.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..4abc791d6 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + __NAME__ + __TITLE_ACTIVITY__ + \ No newline at end of file diff --git a/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/styles.xml b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..75a1d7910 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/platforms/android/build-artifacts/project-template-gradle/settings.gradle b/platforms/android/build-artifacts/project-template-gradle/settings.gradle new file mode 100644 index 000000000..e1226cf41 --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/settings.gradle @@ -0,0 +1,78 @@ +rootProject.name = "__PROJECT_NAME__" +include ':app'//, ':runtime', ':runtime-binding-generator' + +//project(':runtime').projectDir = new File("${System.env.ANDROID_RUNTIME_HOME}/test-app/runtime") +//project(':runtime-binding-generator').projectDir = new File("${System.env.ANDROID_RUNTIME_HOME}/test-app/runtime-binding-generator") + +file("google-services.json").renameTo(file("./app/google-services.json")) + +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import java.nio.file.Paths + +import org.gradle.internal.logging.text.StyledTextOutputFactory +import groovy.json.JsonSlurper +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +def USER_PROJECT_ROOT = "$rootDir/../../" +def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") +def ext = { + appResourcesPath = getProperty("appResourcesPath") + appPath = getProperty("appPath") +} + +def getAppPath = { -> + def relativePathToApp = "app" + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + + if (ext.appPath) { + // when appPath is passed through -PappPath=/path/to/app + // the path could be relative or absolute - either case will work + relativePathToApp = ext.appPath + } else if (nsConfig != null && nsConfig.appPath != null) { + relativePathToApp = nsConfig.appPath + } + + return Paths.get(USER_PROJECT_ROOT).resolve(relativePathToApp).toAbsolutePath() + } + + +def getAppResourcesPath = { -> + def relativePathToAppResources + def absolutePathToAppResources + def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") + def nsConfig + + if (nsConfigFile.exists()) { + nsConfig = new JsonSlurper().parseText(nsConfigFile.getText("UTF-8")) + } + if (ext.appResourcesPath) { + // when appResourcesPath is passed through -PappResourcesPath=/path/to/App_Resources + // the path could be relative or absolute - either case will work + relativePathToAppResources = ext.appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else if (nsConfig != null && nsConfig.appResourcesPath != null) { + relativePathToAppResources = nsConfig.appResourcesPath + absolutePathToAppResources = Paths.get(USER_PROJECT_ROOT).resolve(relativePathToAppResources).toAbsolutePath() + } else { + absolutePathToAppResources = "${getAppPath()}/App_Resources" + } + return absolutePathToAppResources +} + +def applySettingsGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToSettingsGradle = "$appResourcesPath/Android/settings.gradle" + def settingsGradle = file(pathToSettingsGradle) + if (settingsGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${settingsGradle}" + apply from: pathToSettingsGradle + } +} + +applySettingsGradleConfiguration() diff --git a/platforms/android/build-artifacts/project-template-gradle/settings.json b/platforms/android/build-artifacts/project-template-gradle/settings.json new file mode 100644 index 000000000..ced41772c --- /dev/null +++ b/platforms/android/build-artifacts/project-template-gradle/settings.json @@ -0,0 +1,5 @@ +{ + "v8Version": "8.3.110.9", + "ndkRevision": "21.1.6352462", + "mksnapshotParams": "--profile_deserialization --turbo_instruction_scheduling --target_os=android --no-native-code-counters" +} diff --git a/platforms/android/build-artifacts/scripts/release-notes.js b/platforms/android/build-artifacts/scripts/release-notes.js new file mode 100644 index 000000000..1a01a09f4 --- /dev/null +++ b/platforms/android/build-artifacts/scripts/release-notes.js @@ -0,0 +1,269 @@ +// run this using "node release-notes.js", this will prepend to the start of the "../../CHANGELOG.MD" + +var https = require('https'); +var fs = require('fs'); + +var readline = require('readline'); + +var milestoneRequest = { + hostname: 'api.github.com', + path: '/repos/NativeScript/android-runtime/milestones', + method: 'GET', + headers: { + "User-Agent": "AutoRN", + "Authorization": undefined + } +}; + +var issuesRequest = { + hostname: 'api.github.com', + path: undefined, + method: 'GET', + headers: { + "User-Agent": "AutoRN", + "Authorization": undefined + } +}; + +var username, password; +var milestones = {}; + +getCredentials(); + +function getCredentials() { + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.on('SIGINT', function() { + // Ctrl-C + console.log("Canceled."); + process.exit(1); + }); + + rl.question("Username: ", function(usr) { + rl.close(); + username = usr; + getPassword(function(pass) { + password = pass; + listMilestones(); + }); + }) +}; + +function listMilestones() { + + console.log("Request open milestones."); + milestoneRequest.headers.Authorization = "Basic " + Buffer.from(username + ":" + password).toString("base64"); + + var req = https.request(milestoneRequest, gitHubResponse(function (data) { + + if (!(data instanceof Array)) { + console.log("Error. Expected to get array with the issues for the milestone!\n"); + process.exit(1); + } else { + console.log("Open Milestones:"); + data.forEach(function (milestone) { + console.log(" - '" + milestone.title + "'"); + milestones[milestone.title] = milestone; + }); + console.log(); + } + + selectMilestone(); + + })); + req.end(); + + req.on('error', function (e) { + console.error(e); + }); +} + +function selectMilestone() { + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.on('SIGINT', function() { + // Ctrl-C + console.log("Canceled."); + process.exit(1); + }); + + rl.question("Milestone: ", function(milestone) { + rl.close(); + var selectedMilestone = milestones[milestone]; + if (selectedMilestone) { + createReleaseNotes(selectedMilestone); + } else { + console.log("Milestone error!"); + } + }); +} + +function createReleaseNotes(milestone) { + + console.log("Request closed issues in the milestone."); + issuesRequest.headers.Authorization = "Basic " + Buffer.from(username + ":" + password).toString("base64"); + issuesRequest.path = '/repos/NativeScript/android-runtime/issues?milestone=' + milestone.number + '&state=closed'; + + var req = https.request(issuesRequest, gitHubResponse(function (data) { + + if (!(data instanceof Array)) { + console.log("Error. Expected to get array with the milestones!\n"); + process.exit(1); + } else { + var issues = data; + + console.log("Received " + issues.length + " entries."); + + var bugs = issues.filter(function (i) { + return i.labels.filter(function (l) { + return l.name == "bug"; + }).length > 0; + }); + var features = issues.filter(function (i) { + return i.labels.filter(function (l) { + return l.name == "feature"; + }).length > 0; + }); + + var performance = issues.filter(function (i) { + return i.labels.filter(function (l) { + return l.name == "T:Performance"; + }).length > 0; + }); + + console.log(" - " + bugs.length + " fixed (e.g. T:Bug)"); + console.log(" - " + features.length + " new (e.g. T:Feature"); + console.log(" - " + performance.length + " performance (e.g. T:Performance"); + + var md = ""; + + md += milestone.title + "\r\n==\r\n\r\n"; + + printSection("What's New", features); + printSection("Bug Fixes", bugs); + printSection("Performance", performance); + + function printSection(title, issues) { + if (issues.length > 0) { + md += "## " + title + "\r\n\r\n"; + issues.forEach(printIssue); + md += "\r\n"; + } + } + + function printIssue(i) { + md += " - [" + i.title + " (#" + i.number + ")](" + i.html_url + ")\r\n"; + } + + var currentLog = fs.readFileSync("../../CHANGELOG.md"); + var newLog = md + currentLog; + fs.writeFileSync("../../CHANGELOG.md", newLog); + } + + })); + req.end(); + + req.on('error', function (e) { + console.error(e); + }); +} + +function gitHubResponse(f) { + var msg = ""; + + return function(res) { + + console.log(); + + var nlTerminate = false; + if (res.headers['x-ratelimit-limit']) { + console.log("Rate limit: " + res.headers['x-ratelimit-limit']); + nlTerminate = true; + } + if (res.headers['x-ratelimit-remaining']) { + console.log("Rate remaining: " + res.headers['x-ratelimit-remaining']); + nlTerminate = true; + } + if (res.headers['x-ratelimit-reset']) { + console.log("Rate will reset: " + new Date(res.headers['x-ratelimit-reset'] * 1000)); + nlTerminate = true; + } + + if (nlTerminate) { + console.log(); + } + + res.on('data', function (d) { + msg += d; + }); + res.on('end', function (d) { + + var data = JSON.parse(msg); + if (data.message) { + console.log("Message:\n" + data.message + "\n"); + } + if (data.documentation_url) { + console.log("Documentation:\n" + data.documentation_url + "\n"); + } + + f(data); + }); + } +} + +function getPassword(callback) { + process.stdout.write("Password: "); + + var stdin = process.stdin; + stdin.resume(); + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding('utf8'); + + var password = ''; + stdin.on('data', onStdinData); + + function onStdinData(ch) { + ch = ch + ""; + + switch (ch) { + case "\n": + case "\r": + case "\u0004": + // They've finished typing their password + process.stdout.write('\n'); + stdin.setRawMode(false); + stdin.pause(); + callback(password); + stdin.removeListener('data', onStdinData); + break; + case "\u0003": + // Ctrl-C + console.log("Canceled."); + process.exit(1); + break; + case "\u007F": + // Backspace + if (password.length > 0) { + password = password.substring(0, password.length - 1); + process.stdout.write('*\033[<1>D'); + } else { + process.stdout.write(' \033[<1>D'); + } + break; + default: + // More passsword characters + process.stdout.write('*\033[<1>D'); + password += ch; + break; + } + } +} + diff --git a/platforms/android/build.dev.sh b/platforms/android/build.dev.sh new file mode 100755 index 000000000..46e29bf0c --- /dev/null +++ b/platforms/android/build.dev.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# smaller version of build.sh that sets the commit hash to the current git commit hash and uses the package.json version + +echo "Ensure adb is in PATH" +export PATH="$ANDROID_HOME/platform-tools:$PATH" +adb version + +echo "Update submodule" +git submodule update --init + +echo "Cleanup old build and test artefacts" +rm -rf consoleLog.txt +rm -rf test-app/dist/*.xml + +./gradlew cleanRuntime + +./gradlew -PgitCommitVersion=$(git rev-parse HEAD) +cp dist/nativescript-android-*.tgz dist/nativescript-android.tgz + diff --git a/platforms/android/build.gradle b/platforms/android/build.gradle new file mode 100644 index 000000000..fadae2035 --- /dev/null +++ b/platforms/android/build.gradle @@ -0,0 +1,498 @@ +/* +* Usage: +* gradlew - Builds the NativeScript Android App Package using an application project template. + gradlew -PgitCommitVersion - sets the commit version of the build + gradlew -PpreReleaseVersion - sets the pre-release version of the build (as per semver spec "-alpha" value results in 1.0.0-alpha) + gradlew -PnoCCache - set this flag if you don't want CCache to be used in CMake build + +*/ + +defaultTasks 'createPackage' + +import groovy.json.JsonSlurper + +import groovy.json.JsonBuilder +import groovy.json.JsonOutput + +def onlyX86 = project.hasProperty("onlyX86") +def useCCache = !project.hasProperty("noCCache") +def hasNdkVersion = project.hasProperty("ndkVersion") + +def hasHostObjects = project.hasProperty("useHostObjects") + +def asNapiModule = project.hasProperty("asNapiModule") + +if (hasNdkVersion) { + println "Using NDK version " + ndkVersion +} + +def hasEngine = project.hasProperty("engine"); + +def isWinOs = System.properties['os.name'].toLowerCase().contains('windows') +def pVersion = "no package version was provided by build.gradle build" +def arVersion = "no commit sha was provided by build.gradle build" +def generateRegularRuntimePackage = !project.hasProperty("skipUnoptimized") + +def jsEngine = engine ?: 'V8-10' + +def DIST_PATH = "$rootDir/dist_${jsEngine.toLowerCase()}" +def TEST_APP_PATH = "$rootDir/test-app" +def BUILD_TOOLS_PATH = "$TEST_APP_PATH/build-tools" +def DIST_FRAMEWORK_PATH = "$DIST_PATH/framework" + +task checkEnvironmentVariables { + if ("$System.env.JAVA_HOME" == "" || "$System.env.JAVA_HOME" == "null") { + throw new GradleException("Set JAVA_HOME to point to the correct Jdk location\n") + } + + if ("$System.env.ANDROID_HOME" == "" || "$System.env.ANDROID_HOME" == "null") { + throw new GradleException("Set ANDROID_HOME to point to the correct Android SDK location\n") + } + + if ("$System.env.ANDROID_NDK_HOME" == "" || "$System.env.ANDROID_NDK_HOME" == "null" && !project.hasProperty("ndkVersion")) { + logger.warn("Warning: The ANDROID_NDK_HOME is not set nor the ndkVersion project property. The build might fail if a ndk cannot be found.\n") + } + + + if ("$System.env.GIT_COMMIT" == "null" && !project.hasProperty("gitCommitVersion")) { + logger.warn("Warning: The GIT_COMMIT is not set. This NativeScript Android Runtime will not be tagged with the git commit it is build from\n") + } + + if (project.hasProperty("metadataGen") && !file("../android-metadata-generator/dist/tns-android-metadata-generator-0.0.1.tgz").exists()) { + throw new GradleException("android-metadata-generator build output not found and no metadataGen option specified. Build android-metadata-generator first.\n") + } +} + +task cleanDistDir(type: Delete) { + delete DIST_PATH +} + +task createDistDir { + dependsOn 'cleanDistDir' + + doLast { + def distF = new File(DIST_PATH) + distF.mkdirs() + } +} + +def getCCacheVersion = { -> + try { + def ccacheVersionOutput = new ByteArrayOutputStream() + exec { + commandLine "ccache", "--version" + standardOutput = ccacheVersionOutput + } + + return ccacheVersionOutput.toString().trim() + } catch (all) { + return 'CCache not found!' + } +} + +task getPackageVersion { + doLast { + String content = new File("$rootDir/package.json").getText("UTF-8") + def jsonSlurper = new JsonSlurper() + def packageJsonMap = jsonSlurper.parseText(content) + + pVersion = packageJsonMap.version + + println "Using runtime version from package.json '${pVersion}'" + + if (project.hasProperty("packageVersion") && packageVersion != "") { + pVersion += "-" + packageVersion + + println "Using packageVersion property '${pVersion}'" + } + + + if (project.hasProperty("preReleaseVersion")) { + pVersion += "-" + preReleaseVersion + + println "Adding preReleaseVersion property '${pVersion}' to package version" + } + + if(onlyX86) { + pVersion += "-onlyX86" + } + + println "The package version is '${pVersion}'" + } +} + +task getCommitVersion { + doLast { + if (project.hasProperty("gitCommitVersion")) { + println "Using commit version property " + gitCommitVersion + arVersion = gitCommitVersion + } else if ("$System.env.GIT_COMMIT" != "null") { + println "Using commit version environment variable " + "$System.env.GIT_COMMIT" + String content = "$System.env.GIT_COMMIT" + arVersion = content.trim() + } + } +} + +task generateDtsgJar(type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + if (isWinOs) { + commandLine "cmd", "/c", "gradlew", ":dts-generator:jar", "--warning-mode", "all" + } else { + commandLine "./gradlew", ":dts-generator:jar", "--warning-mode", "all" + } + } +} + +task jsParserNPMInstall(type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH/build-tools/jsparser" + if (isWinOs) { + commandLine "cmd", "/c", "npm", "install" + } else { + commandLine "npm", "install" + } + } +} + +task generateSbgJar(type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + if (isWinOs) { + commandLine "cmd", "/c", "gradlew", ":static-binding-generator:jar", "--warning-mode", "all" + } else { + commandLine "./gradlew", ":static-binding-generator:jar", "--warning-mode", "all" + } + } +} + +task generateMdgJar(type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + if (isWinOs) { + commandLine "cmd", "/c", "gradlew", ":android-metadata-generator:jar", "--warning-mode", "all" + } else { + commandLine "./gradlew", ":android-metadata-generator:jar", "--warning-mode", "all" + } + } +} + +task cleanRuntime (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + if (isWinOs) { + commandLine "cmd", "/c", "gradlew", ":runtime:clean", "--warning-mode", "all" + } else { + commandLine "./gradlew", ":runtime:clean", "--warning-mode", "all" + } + } +} + +def getAssembleReleaseBuildArguments = { -> + def arguments = [] + if (isWinOs) { + arguments += ["cmd", "/c", "gradlew"] + } else { + arguments.add("./gradlew") + } + arguments += [":runtime:assembleRelease", "-PpackageVersion=${pVersion}", "-PgitCommitVersion=${arVersion}"] + if (onlyX86) { + arguments.add("-PonlyX86") + } + if (useCCache) { + arguments.add("-PuseCCache") + } + if (hasNdkVersion) { + arguments.add("-PndkVersion="+ndkVersion) + } + + if (hasEngine) { + arguments.add("-Pengine=${engine}") + } + + if (hasHostObjects) { + arguments.add("-PuseHostObjects") + } + + if (asNapiModule) { + arguments.add("-PasNapiModule") + } + + arguments += ["--warning-mode", "all"] + + return arguments +} + +task generateOptimizedRuntimeAar (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + def arguments = getAssembleReleaseBuildArguments() + arguments.add("-Poptimized") + commandLine arguments + } +} + +task generateOptimizedWithInspectorRuntimeAar (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + def arguments = getAssembleReleaseBuildArguments() + arguments.add("-PoptimizedWithInspector") + commandLine arguments + } +} + +task generateRuntimeAar (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + def arguments = getAssembleReleaseBuildArguments() + commandLine arguments + } +} + +task buildJsParser (type: Exec) { + workingDir "$BUILD_TOOLS_PATH/jsparser" + doFirst { + if (isWinOs) { + commandLine "cmd", "/c", "npm", "run", "build" + } else { + commandLine "npm", "run", "build" + } + } +} + +task copyFilesToProjectTemeplate { + doLast { + copy { + from "$TEST_APP_PATH/app/src/debug" + into "$DIST_FRAMEWORK_PATH/app/src/debug" + } + copy { + from "$TEST_APP_PATH/app/src/main/assets/internal" + into "$DIST_FRAMEWORK_PATH/app/src/main/assets/internal" + } + copy { + from "$TEST_APP_PATH/app/src/main/java/com/tns/" + include "*.java" + exclude "NativeScriptApplication.java" + exclude "NativeScriptActivity.java" + into "$DIST_FRAMEWORK_PATH/app/src/main/java/com/tns" + } + copy { + from "$TEST_APP_PATH/app/src/main/java/com/tns/internal" + into "$DIST_FRAMEWORK_PATH/app/src/main/java/com/tns/internal" + } + copy { + from "$BUILD_TOOLS_PATH/static-binding-generator/build/libs/static-binding-generator.jar" + into "$DIST_FRAMEWORK_PATH/build-tools" + } + copy { + from "$BUILD_TOOLS_PATH/android-dts-generator/dts-generator/build/libs/dts-generator.jar" + into "$DIST_FRAMEWORK_PATH/build-tools" + } + copy { + from "$BUILD_TOOLS_PATH/jsparser/build/js_parser.js" + into "$DIST_FRAMEWORK_PATH/build-tools/jsparser" + } + copy { + from "$BUILD_TOOLS_PATH/android-metadata-generator/build/libs/android-metadata-generator.jar" + into "$DIST_FRAMEWORK_PATH/build-tools" + } + copy { + from "$TEST_APP_PATH/runtime/build/outputs/aar/runtime-regular-release.aar" + into "$DIST_FRAMEWORK_PATH/app/libs/runtime-libs" + rename "runtime-regular-release.aar", "nativescript-regular.aar" + } + copy { + from "$TEST_APP_PATH/runtime/build/outputs/aar/runtime-optimized-release.aar" + into "$DIST_FRAMEWORK_PATH/app/libs/runtime-libs" + rename "runtime-optimized-release.aar", "nativescript-optimized.aar" + } + copy { + from "$TEST_APP_PATH/runtime/build/outputs/aar/runtime-optimized-with-inspector-release.aar" + into "$DIST_FRAMEWORK_PATH/app/libs/runtime-libs" + rename "runtime-optimized-with-inspector-release.aar", "nativescript-optimized-with-inspector.aar" + } + copy { + from "$TEST_APP_PATH/app/build.gradle" + into "$DIST_FRAMEWORK_PATH/app" + } + copy { + from "$TEST_APP_PATH/build.gradle" + into "$DIST_FRAMEWORK_PATH" + } + + ant.propertyfile(file: "$TEST_APP_PATH/gradle.properties") { + entry(key: "ns_engine", value: engine) + } + copy { + from "$TEST_APP_PATH/gradle.properties" + into "$DIST_FRAMEWORK_PATH" + } + + ant.propertyfile(file: "$TEST_APP_PATH/gradle.properties") { + entry(key: "ns_engine", value: "V8") + } + + copy { + from "$TEST_APP_PATH/gradle-helpers/paths.gradle" + into "$DIST_FRAMEWORK_PATH/gradle-helpers" + } + copy { + from "$TEST_APP_PATH/gradle-helpers/user_properties_reader.gradle" + into "$DIST_FRAMEWORK_PATH/gradle-helpers" + } + copy { + from "$TEST_APP_PATH/app/gradle-helpers/CustomExecutionLogger.gradle" + into "$DIST_FRAMEWORK_PATH/app/gradle-helpers" + } + copy { + from "$TEST_APP_PATH/app/gradle-helpers/AnalyticsCollector.gradle" + into "$DIST_FRAMEWORK_PATH/app/gradle-helpers" + } + copy { + from "$TEST_APP_PATH/app/gradle-helpers/BuildToolTask.gradle" + into "$DIST_FRAMEWORK_PATH/app/gradle-helpers" + } + copy { + from "$TEST_APP_PATH/gradle" + into "$DIST_FRAMEWORK_PATH/gradle" + } + copy { + from "$TEST_APP_PATH/gradlew" + into "$DIST_FRAMEWORK_PATH" + } + copy { + from "$TEST_APP_PATH/gradlew.bat" + into "$DIST_FRAMEWORK_PATH" + } + } +} + +task copyProjectTemplate(type: Copy) { + from "$rootDir/build-artifacts/project-template-gradle" + into "$DIST_FRAMEWORK_PATH" +} + +task copyPackageJson(type: Copy) { + from "$rootDir/package.json" + into "$DIST_PATH" +} + +task setPackageVersionInPackageJsonFile { + doLast { + def inputFile = new File("$DIST_PATH/package.json") + def json = new JsonSlurper().parseText(inputFile.text) + json.version = pVersion + def jb = new JsonBuilder(json) + inputFile.text = JsonOutput.prettyPrint(jb.toString()) + } +} + +task copyReadme(type: Copy) { + from "README.md" + into "$DIST_PATH" +} + +task createNpmPackage(type: Exec) { + doFirst { + workingDir "$DIST_PATH" + + if (isWinOs) { + commandLine "cmd", "/c", "npm", "pack" + } else { + commandLine "npm", "pack" + } + } +} + +generateSbgJar.dependsOn(generateDtsgJar) +generateSbgJar.dependsOn(jsParserNPMInstall) +generateMdgJar.dependsOn(generateSbgJar) +buildJsParser.dependsOn(jsParserNPMInstall) +createDistDir.dependsOn(generateMdgJar) + +getPackageVersion.dependsOn(createDistDir) +getCommitVersion.dependsOn(getPackageVersion) +generateOptimizedRuntimeAar.dependsOn(getCommitVersion) + +generateOptimizedWithInspectorRuntimeAar.dependsOn(generateOptimizedRuntimeAar) + +if (generateRegularRuntimePackage) { + generateRuntimeAar.dependsOn(generateOptimizedWithInspectorRuntimeAar) + buildJsParser.dependsOn(generateRuntimeAar) +} else { + buildJsParser.dependsOn(generateOptimizedWithInspectorRuntimeAar) +} + +copyFilesToProjectTemeplate.dependsOn(buildJsParser) +copyProjectTemplate.dependsOn(copyFilesToProjectTemeplate) +copyPackageJson.dependsOn(copyProjectTemplate) +setPackageVersionInPackageJsonFile.dependsOn(copyPackageJson) +copyReadme.dependsOn(setPackageVersionInPackageJsonFile) +createNpmPackage.dependsOn(copyReadme) + +task createPackage { + println "CCache version: " + getCCacheVersion() + + description "Builds the NativeScript Android cleanBuildArtefactsApp Package using an application project template." + dependsOn createNpmPackage + println "Creating NativeScript Android Package" +} + +task runAstTests (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH/build-tools/jsparser/tests" + if (isWinOs) { + commandLine "cmd", "/c", "npm", "test" + } else { + commandLine "npm", "test" + } + } +} + +task runSbgTests (type: Exec, dependsOn: 'runAstTests') { + doFirst { + workingDir "$TEST_APP_PATH" + if (isWinOs) { + commandLine "cmd", "/c", "gradlew", ":static-binding-generator:test", "--warning-mode", "all" + } else { + commandLine "./gradlew", ":static-binding-generator:test", "--warning-mode", "all" + } + } +} + +def getRunTestsBuildArguments = { taskName -> + def arguments = [] + if (isWinOs) { + arguments += ["cmd", "/c", "gradlew"] + } else { + arguments.add("./gradlew") + } + arguments += ["-b", "runtests.gradle", taskName] + if (onlyX86) { + arguments.add("-PonlyX86") + } + if (useCCache) { + arguments.add("-PuseCCache") + } + arguments += ["--warning-mode", "all"] + return arguments +} + +task runTests (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + commandLine getRunTestsBuildArguments("runtests") + } +} + +task runTestsAndVerifyResults (type: Exec) { + doFirst { + workingDir "$TEST_APP_PATH" + commandLine getRunTestsBuildArguments("runtestsAndVerifyResults") + } +} + +runSbgTests.dependsOn(generateMdgJar) +runTests.dependsOn(runSbgTests) diff --git a/platforms/android/build.sh b/platforms/android/build.sh new file mode 100755 index 000000000..2f42b49c9 --- /dev/null +++ b/platforms/android/build.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +############################################################################################### +# Android Runtime build script for CI. +# This file is used by the CI only and it's not meant for regular development. +############################################################################################### + +echo "Ensure adb is in PATH" +export PATH="$ANDROID_HOME/platform-tools:$PATH" +adb version + +echo "Update submodule" +git submodule update --init + +echo "Cleanup old build and test artefacts" +rm -rf consoleLog.txt +rm -rf test-app/dist/*.xml + +echo "Stopping running emulators if any" +for KILLPID in `ps ax | grep 'emulator' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done +for KILLPID in `ps ax | grep 'qemu' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done +for KILLPID in `ps ax | grep 'adb' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done + +./gradlew cleanRuntime +if [ "$1" != 'unit_tests_only' ]; then + echo "Building Android Runtime with paramerter packageVersion: $ANDROID_PACKAGE_VERSION and commit: $GIT_COMMIT" + if [ "$NO_CCACHE" == 'true' ]; then + ./gradlew -PpackageVersion=$ANDROID_PACKAGE_VERSION -PgitCommitVersion=$GIT_COMMIT -PnoCCache + else + ./gradlew -PpackageVersion=$ANDROID_PACKAGE_VERSION -PgitCommitVersion=$GIT_COMMIT + fi + + cp dist/nativescript-android-*.tgz dist/nativescript-android.tgz +else + echo "Building Android Runtime for x86 unit tests with paramerter packageVersion: $ANDROID_PACKAGE_VERSION and commit: $GIT_COMMIT" + if [ "$NO_CCACHE" == 'true' ]; then + ./gradlew -PpackageVersion=$ANDROID_PACKAGE_VERSION -PgitCommitVersion=$GIT_COMMIT -PskipUnoptimized -PonlyX86 -PnoCCache + else + ./gradlew -PpackageVersion=$ANDROID_PACKAGE_VERSION -PgitCommitVersion=$GIT_COMMIT -PskipUnoptimized -PonlyX86 + fi + cp dist/nativescript-android-*.tgz dist/nativescript-android.tgz +fi + +if [ "$2" != '' ]; then + listOfEmulators=$2 +else + listOfEmulators="Emulator-Api28-Google Emulator-Api23-Default Emulator-Api19-Default" +fi + +# Run static binding generator unit tests +./gradlew runSbgTests + +for emulator in $listOfEmulators; do + echo "Start emulator $emulator" + $ANDROID_HOME/emulator/emulator -avd ${emulator} -verbose -wipe-data -gpu on& + find ~/.android/avd/${emulator}.avd -type f -name 'config.ini' -exec cat {} + + + echo "Run Android Runtime unit tests for $emulator" + $ANDROID_HOME/platform-tools/adb wait-for-device + $ANDROID_HOME/platform-tools/adb -s emulator-5554 logcat -c + $ANDROID_HOME/platform-tools/adb -s emulator-5554 logcat > consoleLog.txt& + $ANDROID_HOME/platform-tools/adb -s emulator-5554 logcat > consoleLog$emulator.txt& + + if [ "$1" != 'unit_tests_only' ]; then + ./gradlew runtests + else + ./gradlew runtests -PonlyX86 + fi + + echo "Rename unit test result" + ( + cd ./test-app/dist + mv android_unit_test_results.xml $emulator.xml + ) + + echo "Stopping running emulators" + for KILLPID in `ps ax | grep 'emulator' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done + for KILLPID in `ps ax | grep 'qemu' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done + for KILLPID in `ps ax | grep 'adb' | grep -v 'grep' | awk ' { print $1;}'`; do kill -9 $KILLPID; done +done + +echo $cwd +cd $cwd + diff --git a/platforms/android/debug.keystore b/platforms/android/debug.keystore new file mode 100644 index 000000000..364e105ed Binary files /dev/null and b/platforms/android/debug.keystore differ diff --git a/platforms/android/docs/extending-inspector.md b/platforms/android/docs/extending-inspector.md new file mode 100644 index 000000000..583b56301 --- /dev/null +++ b/platforms/android/docs/extending-inspector.md @@ -0,0 +1,77 @@ +# How to extend the V8 inspector + +## Overview + +> Note: Check out the blog post [Chrome DevTools Integration from a Technical Perspective](https://www.nativescript.org/blog/chrome-devtools-integration) for an overview of how the v8 inspector in NativeScript works. + +The default inspector of the V8 project comes with only a [handful of implemented inspector domains](https://chromedevtools.github.io/devtools-protocol/v8). This is more than enough to be able to debug JavaScript, but what if you want to take it a step further and enable inspection of any arbitrary files of your application, need to spoof the network requests, or inspect the visual elements? + +## Getting additional protocol domain definitions + +This step involves fetching and building the V8 projectory, explanation for which can be found at [Building V8 with GN](https://github.com/v8/v8/wiki/Building-with-GN). + +> Note: When passing arguments to gn to have a project generate, make sure to include the inspector sources. For an overview of all available gn arguments run `gn args project-dir --list` + +1. Modify `v8/include/js_protocol.pdl` to only contain the domains that will be used in the runtime project. The right way to go about this is to copy-paste the `js_protocol` from the runtime project, and add on top of it. For a complete list of all protocol domains supported by the Chrome browser refer to [browser_protocol.json](https://chromium.googlesource.com/chromium/src/+log/master/third_party/WebKit/Source/core/inspector/browser_protocol.json) in the Chromium project. Example browser_protocol.json file can be found [here](https://chromedevtools.github.io/devtools-protocol/tot/). +2. Modify `v8/src/inspector/inspector_protocol_config.json` and include the names for the additional protocol domain definitions that would need to be generated. +```json +{ + "protocol": { + ... + "options": [ + { + "domain": "Schema", + "exported": ["Domain"] + }, + ... + { + "domain": "Console" + }, + { + "domain": "Log" + }, + { + "domain": "Overlay" + } + ] + }, + + "exported": { + ... + }, + + "lib": { + ... + } +} +``` +3. Figure out which parts of the big `js_inspector.json` protocol you will want to keep for the inspector implementation. It is likely that a lot of it will be browser-specific, so you don't need to spend extra time doing stub implementations for methods and events the application will not be making notifications for. + +4. Run the ninja build. Upon completion, the inspector protocol files would be at `outgn/$ARCH-release/gen/src/inspector/protocol`. + +5. Copy-Paste all `.cpp` and `.h` files in the runtime project at `test-app/runtime/src/main/cpp/v8_inspector/src/inspector/protocol` + +6. Create a new C++ class extending the desired Domain (e.g. DOM). Name it according to the convention already established by the V8 team - `v8--agent-impl.h/cpp` - See [v8-dom-agent-impl.h](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-dom-agent-impl.h#L18). Implement the Backend::'s methods in the `.cpp` file. + +7. To implement event handlers of a certain domain, check out DomainCallbackHandlers ([DOMDomainCallbackHandlers](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/DOMDomainCallbackHandlers.h#L14)) which are registered in [JsV8InspectorClient.cpp](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp#L237) + +8. Register the newly created agent implementations in [v8-inspector-session-impl.h/cc](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.h#L19) - `V8InspectorSessionImpl` class: + +8.1. Changes in v8-inspector-session-impl.`h` + - `#include "src/inspector/protocol/.h` + - add forward class declaration of the agent implementation - `class V8AgentImpl;` + - create a private `std::unique_ptrAgentImpl> m_Agent;` field + - create a public getter method `Agent()` + +8.2. Changes in v8-inspector-session-impl.`cc` + - `#include "src/inspector/protocol/v8--agent-impl.h` + - register protocol domain command prefix in [canDispatchMethod()](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L31) + - in the V8InspectorSessionImpl constructor initialize the new agent implementation with a [nullptr](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L86) + - [create a unique pointer wrapper for your new agent impl instance](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L141), and call the static domain dispatcher's [wire](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L143) method + - make sure to call [agent.disable](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L167) in V8InspectorSessionImpl's destructor. + - register the newly implemented domain as a supported one inside [ V8InspectorSessionImpl::supportedDomainsImpl()](https://github.com/NativeScript/android-runtime/blob/5a04e09439e2bc6a201577895b9ac6538441e758/test-app/runtime/src/main/cpp/v8_inspector/src/inspector/v8-inspector-session-impl.cc#L389) + - `#include "NSV8DebuggerAgentImpl.h"` + - replace **V8DebuggerAgentImpl** with **NSV8DebuggerAgentImpl** + +9. Don't forget to add the new classes to the CMakeLists! +11. Test whether the new domain agent is registered by opening Chrome DevTools. In order to debug the Chrome DevTools frontend, you could open the Chrome DevTools inside an Android Chrome DevTools session - Ctrl(Cmd) + Shift + I. diff --git a/platforms/android/gradle/wrapper/gradle-wrapper.jar b/platforms/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..249e5832f Binary files /dev/null and b/platforms/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/platforms/android/gradle/wrapper/gradle-wrapper.properties b/platforms/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..3ae1e2f12 --- /dev/null +++ b/platforms/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/platforms/android/gradlew b/platforms/android/gradlew new file mode 100755 index 000000000..a69d9cb6c --- /dev/null +++ b/platforms/android/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/platforms/android/gradlew.bat b/platforms/android/gradlew.bat new file mode 100644 index 000000000..f127cfd49 --- /dev/null +++ b/platforms/android/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/platforms/android/package.json b/platforms/android/package.json new file mode 100644 index 000000000..fe10844e2 --- /dev/null +++ b/platforms/android/package.json @@ -0,0 +1,38 @@ +{ + "name": "@nativescript/android", + "description": "NativeScript for Android using Node-API", + "version": "8.8.5", + "repository": { + "type": "git", + "url": "https://github.com/NativeScript/android.git" + }, + "files": [ + "**/*" + ], + "version_info": { + "gradle": "8.14.3", + "gradleAndroid": "8.12.1", + "ndk": "r27", + "ndkApiLevel": "21", + "minSdk": "21", + "compileSdk": "34", + "buildTools": "34.0.0", + "kotlin": "2.0.0" + }, + "// this gradle key is here for backwards compatibility - we'll phase it out slowly...": "", + "gradle": { + "version": "8.14.3", + "android": "8.12.1" + }, + "scripts": { + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", + "version": "npm run changelog && git add CHANGELOG.md", + "build": "node ./scripts/build.js", + "setup": "cd test-app/build-tools/jsparser && npm install && cd ../../../" + }, + "devDependencies": { + "conventional-changelog-cli": "^2.1.1", + "dayjs": "^1.11.7", + "semver": "^7.5.0" + } +} diff --git a/platforms/android/scripts/build.js b/platforms/android/scripts/build.js new file mode 100644 index 000000000..b9e1c854f --- /dev/null +++ b/platforms/android/scripts/build.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +// Simple build helper for NativeScript Android runtime. +// Supports interactive prompts or direct CLI flags. +// See README in the repo for Gradle usage. + +const { spawn } = require('child_process'); +const path = require('path'); +const readline = require('readline'); + +const VALID_ENGINES = ['V8-10',"V8-11","V8-13", 'QUICKJS', "QUICKJS_NG", 'HERMES', 'JSC', 'SHERMES', 'PRIMJS']; +const HOST_OBJECTS_SUPPORTED = new Set(['V8-10','V8-11',"V8-13", 'QUICKJS',"QUICKJS_NG", 'PRIMJS']); + +function parseArgs(argv) { + const opts = {}; + argv.forEach(arg => { + if (!arg.startsWith('--')) return; + const eq = arg.indexOf('='); + if (eq !== -1) { + const k = arg.slice(2, eq); + const v = arg.slice(eq + 1); + opts[k] = v; + } else { + const k = arg.slice(2); + // flag -> boolean true + opts[k] = true; + } + }); + return opts; +} + +async function prompt(question, rl, defaultVal) { + return new Promise(resolve => { + const q = defaultVal ? `${question} (${defaultVal}) ` : `${question} `; + rl.question(q, ans => { + if (!ans && typeof defaultVal !== 'undefined') return resolve(defaultVal); + resolve(ans); + }); + }); +} + +async function interactiveFill(opts) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + try { + // If skip-all-args is given, only ask for engine and host-objects (when supported). + if (opts['skip-all-args']) { + if (!opts.engine) { + console.log('Select JS engine:'); + VALID_ENGINES.forEach((e, i) => console.log(` ${i + 1}) ${e}`)); + const ans = await prompt('Choose number or name', rl, 'V8-10'); + const pick = /^\d+$/.test(ans) ? VALID_ENGINES[Number(ans) - 1] : ans; + opts.engine = VALID_ENGINES.includes(pick) ? pick : 'V8-10'; + } + + // Only prompt for host objects if the chosen engine supports it + if (HOST_OBJECTS_SUPPORTED.has(opts.engine)) { + if (typeof opts['use-host-objects'] === 'undefined') { + const ans = await prompt('Use host objects? [y/N]', rl, 'N'); + if (/^y(es)?$/i.test(ans)) opts['use-host-objects'] = true; + } + } else { + // ensure the flag is not set for unsupported engines + if (opts['use-host-objects']) { + console.log(`Warning: host objects not supported for engine ${opts.engine}; ignoring --use-host-objects`); + delete opts['use-host-objects']; + } + } + + return opts; + } + + // original interactive flow (unchanged) when not skipping all args + if (!opts.engine) { + console.log('Select JS engine:'); + VALID_ENGINES.forEach((e, i) => console.log(` ${i + 1}) ${e}`)); + const ans = await prompt('Choose number or name', rl, 'V8'); + const pick = /^\d+$/.test(ans) ? VALID_ENGINES[Number(ans) - 1] : ans; + opts.engine = VALID_ENGINES.includes(pick) ? pick : 'V8'; + } + + const booleanPrompts = [ + { key: 'use-host-objects', prop: 'useHostObjects', desc: 'Use host objects (useHostObjects)' }, + ]; + + for (const p of booleanPrompts) { + // skip host-objects prompt if the selected engine does not support it + if (p.key === 'use-host-objects' && !HOST_OBJECTS_SUPPORTED.has(opts.engine)) { + if (opts['use-host-objects']) { + console.log(`Warning: host objects not supported for engine ${opts.engine}; ignoring --use-host-objects`); + delete opts['use-host-objects']; + } + continue; + } + + if (typeof opts[p.key] === 'undefined') { + const ans = await prompt(`${p.desc}? [y/N]`, rl, 'N'); + if (/^y(es)?$/i.test(ans)) opts[p.key] = true; + } + } + + } finally { + rl.close(); + } + + return opts; +} + +function buildGradleArgs(opts) { + const props = []; + if (opts.engine) props.push(`-Pengine=${opts.engine}`); + if (opts['use-host-objects']) props.push('-PuseHostObjects'); + if (opts['as-napi-module']) props.push('-PasNapiModule'); + + return props; +} + +async function main() { + const initial = parseArgs(process.argv.slice(2)); + const opts = await interactiveFill(initial); + + const gradleProps = buildGradleArgs(opts); + const gradlew = process.platform === 'win32' ? 'gradlew' : './gradlew'; + const gradleCmd = [gradlew].concat(gradleProps, []); + + console.log('\nGradle command:'); + console.log(gradleCmd.join(' '), '\n'); + + if (opts['dry-run']) { + console.log('Dry run requested. Exiting without executing gradle.'); + return; + } + + const proc = spawn(gradleCmd[0], gradleCmd.slice(1), { stdio: 'inherit', cwd: process.cwd(), shell: false }); + + proc.on('exit', code => { + if (code === 0) { + console.log('\nBuild finished successfully.'); + } else { + console.error(`\nBuild failed with exit code ${code}.`); + } + process.exit(code); + }); + + proc.on("message", (message) => { + console.log(message); + }) + + proc.on('error', err => { + console.error('Failed to start gradle:', err.message || err); + process.exit(1); + }); +} + +main().catch(err => { + console.error('Error:', err && err.message ? err.message : err); + process.exit(1); +}); \ No newline at end of file diff --git a/platforms/android/scripts/get-next-version.js b/platforms/android/scripts/get-next-version.js new file mode 100644 index 000000000..ac3912888 --- /dev/null +++ b/platforms/android/scripts/get-next-version.js @@ -0,0 +1,64 @@ +const semver = require("semver"); +const child_process = require("child_process"); +const dayjs = require("dayjs"); +const fs = require("fs"); + +const currentVersion = + process.env.NPM_VERSION || require("../package.json").version; + +if (!currentVersion) { + throw new Error("Invalid current version"); +} +const currentTag = process.env.NPM_TAG || "next"; +const runID = process.env.GITHUB_RUN_ID || 0; + +let prPrerelease = ""; + +if (currentTag === "pr" && process.env.GITHUB_EVENT_PATH) { + try { + const ev = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); + const prNum = ev.pull_request.number; + // add extra PR number to version-pr.PRNUM-.... + prPrerelease = `${prNum}-`; + } catch (e) { + // don't add pr prerelease + } +} + +const preRelease = `${currentTag}.${prPrerelease}${dayjs().format("YYYY-MM-DD")}-${runID}`; + +let lastTagVersion = ( + process.env.LAST_TAGGED_VERSION || + child_process + .spawnSync("git", ["describe", "--tags", "--abbrev=0", "--match=v*"]) + .stdout.toString() +) + .trim() + .substring(1); +if (!semver.parse(lastTagVersion)) { + throw new Error("Invalid last tag version"); +} + +function setPreRelease(version) { + const parsed = semver.parse(version); + return semver.parse( + `${parsed.major}.${parsed.minor}.${parsed.patch}-${preRelease}` + ); +} + +let nextVersion = setPreRelease(currentVersion); + +if (!nextVersion) { + throw new Error("Invalid next version"); +} + +if (semver.compare(currentVersion, lastTagVersion) <= 0) { + // next version is older than current version + nextVersion = setPreRelease(semver.parse(lastTagVersion).inc("patch")); +} + +if (!nextVersion) { + throw new Error("Invalid next version"); +} + +console.log(nextVersion.format()); diff --git a/platforms/android/scripts/get-npm-tag.js b/platforms/android/scripts/get-npm-tag.js new file mode 100644 index 000000000..68cd158cd --- /dev/null +++ b/platforms/android/scripts/get-npm-tag.js @@ -0,0 +1,19 @@ +const semver = require("semver"); + +const currentVersion = + process.env.NPM_VERSION || require("../package.json").version; + +function validateNpmTag(version) { + const parsed = semver.parse(version); + return ( + parsed.prerelease.length === 0 || /^[a-zA-Z]+$/.test(parsed.prerelease[0]) + ); +} + +function getNpmTag(version) { + if (!validateNpmTag(version)) throw new Error("Invalid npm tag"); + const parsed = semver.parse(version); + return parsed.prerelease[0] || "latest"; +} + +console.log(getNpmTag(currentVersion)); diff --git a/platforms/android/test-app/.gitignore b/platforms/android/test-app/.gitignore new file mode 100644 index 000000000..e94ad2aa9 --- /dev/null +++ b/platforms/android/test-app/.gitignore @@ -0,0 +1,26 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild +app/app.iml + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/dictionaries +.idea/libraries + + +treeNodeStream.dat +treeStringsStream.dat +treeValueStream.dat +NativeScriptActivity.java +NativeScriptApplication.java +**/com/tns/gen \ No newline at end of file diff --git a/platforms/android/test-app/additional_gradle.properties b/platforms/android/test-app/additional_gradle.properties new file mode 100644 index 000000000..af92e00cc --- /dev/null +++ b/platforms/android/test-app/additional_gradle.properties @@ -0,0 +1,15 @@ +# To debug Gradle scripts, you must enable this behaviour with a flag (seen some rows below) +# Start debugging by initiating a remote Java debug through your IDE on port 5005 +# Beware that Gradle spawns daemons to execute secondary builds. This could be a problem when debugging. +# You could stop them with "gradle --stop" and run a given task from the console with "gradle some_task_name --no-daemon" +# or with "org.gradle.daemon=false" set inside your "~/.gradle/gradle.properties" file. +# Uncomment the following line in order to enable Gradle debugging. +# org.gradle.debug=true + +useKotlin=true +gatherAnalyticsData=true + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/platforms/android/test-app/analytics/build-statistics.json b/platforms/android/test-app/analytics/build-statistics.json new file mode 100644 index 000000000..a972bc513 --- /dev/null +++ b/platforms/android/test-app/analytics/build-statistics.json @@ -0,0 +1,6 @@ +{ + "kotlinUsage": { + "hasUseKotlinPropertyInApp": true, + "hasKotlinRuntimeClasses": false + } +} \ No newline at end of file diff --git a/platforms/android/test-app/app/.gitignore b/platforms/android/test-app/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/platforms/android/test-app/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/platforms/android/test-app/app/build.gradle b/platforms/android/test-app/app/build.gradle new file mode 100644 index 000000000..5d7846de7 --- /dev/null +++ b/platforms/android/test-app/app/build.gradle @@ -0,0 +1,1333 @@ +/* +* Script builds apk in release or debug mode +* To run: +* gradle assembleRelease -Prelease (release mode) +* gradle assembleDebug (debug mode -> default) +* Options: +* -Prelease //this flag will run build in release mode +* -PksPath=[path_to_keystore_file] +* -PksPassword=[password_for_keystore_file] +* -Palias=[alias_to_use_from_keystore_file] +* -Ppassword=[password_for_alias] +* +* -PtargetSdk=[target_sdk] +* -PbuildToolsVersion=[build_tools_version] +* -PcompileSdk=[compile_sdk_version] +* -PandroidXLegacy=[androidx_legacy_version] +* -PandroidXAppCompat=[androidx_appcompat_version] +* -PandroidXMaterial=[androidx_material_version] +* -PappPath=[app_path] +* -PappResourcesPath=[app_resources_path] +*/ + +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import org.apache.commons.io.FileUtils + +import javax.inject.Inject +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.security.MessageDigest +import java.util.jar.JarEntry +import java.util.jar.JarFile + +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +apply plugin: "com.android.application" +apply from: "gradle-helpers/BuildToolTask.gradle" +apply from: "gradle-helpers/CustomExecutionLogger.gradle" +apply from: "gradle-helpers/AnalyticsCollector.gradle" +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' + +def onlyX86 = project.hasProperty("onlyX86") +if (onlyX86) { + outLogger.withStyle(Style.Info).println "OnlyX86 build triggered." +} + +//common +def BUILD_TOOLS_PATH = "$rootDir/build-tools" +def PASSED_TYPINGS_PATH = System.getenv("TNS_TYPESCRIPT_DECLARATIONS_PATH") +def TYPINGS_PATH = "$BUILD_TOOLS_PATH/typings" +if (PASSED_TYPINGS_PATH != null) { + TYPINGS_PATH = PASSED_TYPINGS_PATH +} + +def PACKAGE_JSON = "package.json" + +//static binding generator +def SBG_JAVA_DEPENDENCIES = "sbg-java-dependencies.txt" +def SBG_INPUT_FILE = "sbg-input-file.txt" +def SBG_OUTPUT_FILE = "sbg-output-file.txt" +def SBG_JS_PARSED_FILES = "sbg-js-parsed-files.txt" +def SBG_BINDINGS_NAME = "sbg-bindings.txt" +def SBG_INTERFACE_NAMES = "sbg-interface-names.txt" +def INPUT_JS_DIR = "$projectDir/src/main/assets/app" +def OUTPUT_JAVA_DIR = "$projectDir/src/main/java" +def APP_DIR = "$projectDir/src/main/assets/app" + +//metadata generator +def MDG_OUTPUT_DIR = "mdg-output-dir.txt" +def MDG_JAVA_DEPENDENCIES = "mdg-java-dependencies.txt" +def METADATA_OUT_PATH = "$projectDir/src/main/assets/metadata" +def METADATA_JAVA_OUT = "mdg-java-out.txt" + +// paths to jar libraries +def pluginsJarLibraries = new LinkedList() +def allJarLibraries = new LinkedList() + +def computeCompileSdkVersion = { -> + project.hasProperty("compileSdk") ? compileSdk : NS_DEFAULT_COMPILE_SDK_VERSION as int +} +def computeTargetSdkVersion = { -> + project.hasProperty("targetSdk") ? targetSdk : NS_DEFAULT_COMPILE_SDK_VERSION as int +} +def computeMinSdkVersion = { -> + project.hasProperty("minSdk") ? minSdk : NS_DEFAULT_MIN_SDK_VERSION as int +} +def computeBuildToolsVersion = { -> + project.hasProperty("buildToolsVersion") ? buildToolsVersion : NS_DEFAULT_BUILD_TOOLS_VERSION as String +} + +def enableAnalytics = (project.hasProperty("gatherAnalyticsData") && project.gatherAnalyticsData == "true") +def enableVerboseMDG = project.gradle.startParameter.logLevel.name() == 'DEBUG' +def analyticsFilePath = "$rootDir/analytics/build-statistics.json" +def analyticsCollector = project.ext.AnalyticsCollector.withOutputPath(analyticsFilePath) +if (enableAnalytics) { + analyticsCollector.markUseKotlinPropertyInApp(true) + analyticsCollector.writeAnalyticsFile() +} + +project.ext.selectedBuildType = project.hasProperty("release") ? "release" : "debug" + +buildscript { + def applyBuildScriptConfigurations = { -> + def absolutePathToAppResources = getAppResourcesPath() + def pathToBuildScriptGradle = "$absolutePathToAppResources/Android/buildscript.gradle" + def buildScriptGradle = file(pathToBuildScriptGradle) + if (buildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from ${buildScriptGradle}" + apply from: pathToBuildScriptGradle, to: buildscript + } + + nativescriptDependencies.each { dep -> + def pathToPluginBuildScriptGradle = "$rootDir/${dep.directory}/$PLATFORMS_ANDROID/buildscript.gradle" + def pluginBuildScriptGradle = file(pathToPluginBuildScriptGradle) + if (pluginBuildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from dependency ${pluginBuildScriptGradle}" + apply from: pathToPluginBuildScriptGradle, to: buildscript + } + } + } + applyBuildScriptConfigurations() +} +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// CONFIGURATIONS /////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +def applyBeforePluginGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToBeforePluginGradle = "$appResourcesPath/Android/before-plugins.gradle" + def beforePluginGradle = file(pathToBeforePluginGradle) + if (beforePluginGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${beforePluginGradle}" + apply from: pathToBeforePluginGradle + } +} + +def applyAppGradleConfiguration = { -> + def appResourcesPath = getAppResourcesPath() + def pathToAppGradle = "$appResourcesPath/Android/app.gradle" + def appGradle = file(pathToAppGradle) + if (appGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined configuration from ${appGradle}" + apply from: pathToAppGradle + } else { + outLogger.withStyle(Style.Info).println "\t + couldn't load user-defined configuration from ${appGradle}. File doesn't exist." + } +} + +def applyPluginGradleConfigurations = { -> + nativescriptDependencies.each { dep -> + def includeGradlePath = "$rootDir/${dep.directory}/$PLATFORMS_ANDROID/include.gradle" + if (file(includeGradlePath).exists()) { + apply from: includeGradlePath + } + } +} + +def getAppIdentifier = { packageJsonMap -> + def appIdentifier = "" + if (packageJsonMap && packageJsonMap.nativescript) { + appIdentifier = packageJsonMap.nativescript.id + if (!(appIdentifier instanceof String)) { + appIdentifier = appIdentifier.android + } + } + + return appIdentifier +} + +def setAppIdentifier = { -> + outLogger.withStyle(Style.SuccessHeader).println "\t + setting applicationId" + File packageJsonFile = new File("$USER_PROJECT_ROOT/$PACKAGE_JSON") + + if (packageJsonFile.exists()) { + def content = packageJsonFile.getText("UTF-8") + def jsonSlurper = new JsonSlurper() + def packageJsonMap = jsonSlurper.parseText(content) + def appIdentifier = getAppIdentifier(packageJsonMap) + + if (appIdentifier) { + project.ext.nsApplicationIdentifier = appIdentifier + android.defaultConfig.applicationId = appIdentifier + android.namespace = appIdentifier + } + } +} + +def computeNamespace = { -> + def appPackageJsonFile = file("${APP_DIR}/$PACKAGE_JSON") + + if (appPackageJsonFile.exists()) { + def content = appPackageJsonFile.getText("UTF-8") + + def jsonSlurper = new JsonSlurper() + def packageJsonMap = jsonSlurper.parseText(content) + + def appIdentifier = "" + + if (packageJsonMap) { + if (packageJsonMap.android && packageJsonMap.android.id) { + appIdentifier = packageJsonMap.android.id + } else if (packageJsonMap.id) { + appIdentifier = packageJsonMap.id + } + } + + if (appIdentifier) { + return appIdentifier + } + } + return "com.tns.testapplication" +} + +android { + + namespace computeNamespace() + + applyBeforePluginGradleConfiguration() + + kotlinOptions { + jvmTarget = '17' + } + + compileSdk computeCompileSdkVersion() + buildToolsVersion = computeBuildToolsVersion() + + defaultConfig { + def manifest = new XmlSlurper().parse(file(android.sourceSets.main.manifest.srcFile)) + def minSdkVer = manifest."uses-sdk"."@android:minSdkVersion".text() ?: computeMinSdkVersion() + minSdkVersion minSdkVer + targetSdkVersion computeTargetSdkVersion() + ndk { + if (onlyX86) { + abiFilters 'x86' + } else { + abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + } + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + sourceSets.main { + jniLibs.srcDirs = ["$projectDir/libs/jni", "$projectDir/snapshot-build/build/ndk-build/libs", "$projectDir/src/main/jniLibs"] + } + + signingConfigs { + release { + if (project.hasProperty("release")) { + if (project.hasProperty("ksPath") && + project.hasProperty("ksPassword") && + project.hasProperty("alias") && + project.hasProperty("password")) { + + storeFile file(ksPath) + storePassword ksPassword + keyAlias alias + keyPassword password + } + } + } + } + + buildTypes { + release { + if (project.hasProperty("release")) { + if (project.hasProperty("ksPath") && + project.hasProperty("ksPassword") && + project.hasProperty("alias") && + project.hasProperty("password")) { + signingConfig signingConfigs.release + } + } else { + signingConfig signingConfigs.debug + } + } + } + + setAppIdentifier() + applyPluginGradleConfigurations() + applyAppGradleConfiguration() + + def initializeMergedAssetsOutputPath = { -> + android.applicationVariants.configureEach { variant -> + if (variant.buildType.name == project.selectedBuildType) { + def task + if (variant.metaClass.respondsTo(variant, "getMergeAssetsProvider")) { + def provider = variant.getMergeAssetsProvider() + task = provider.get() + } else { + // fallback for older android gradle plugin versions + task = variant.getMergeAssets() + } + for (File file : task.getOutputs().getFiles()) { + if (!file.getPath().contains("${File.separator}incremental${File.separator}")) { + project.ext.mergedAssetsOutputPath = file.getPath() + break + } + } + } + } + } + + initializeMergedAssetsOutputPath() +} + +def externalRuntimeExists = !findProject(':runtime').is(null) +def pluginDependencies + +repositories { + // used for local *.AAR files + pluginDependencies = nativescriptDependencies.collect { + "$rootDir/${it.directory}/$PLATFORMS_ANDROID" + } + + // some plugins may have their android dependencies in a /libs subdirectory + pluginDependencies.addAll(nativescriptDependencies.collect { + "$rootDir/${it.directory}/$PLATFORMS_ANDROID/libs" + }) + + if (!externalRuntimeExists) { + pluginDependencies.add("libs/runtime-libs") + } + + def appResourcesPath = getAppResourcesPath() + def localAppResourcesLibraries = "$appResourcesPath/Android/libs" + + pluginDependencies.add(localAppResourcesLibraries) + + if (pluginDependencies.size() > 0) { + flatDir { + dirs pluginDependencies + } + } + + mavenCentral() +} + +dependencies { + // println "\t ~ [DEBUG][app] build.gradle - ns_default_androidx_appcompat_version = ${ns_default_androidx_appcompat_version}..." + + def androidXAppCompatVersion = "${ns_default_androidx_appcompat_version}" + if (project.hasProperty("androidXAppCompat")) { + androidXAppCompatVersion = androidXAppCompat + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.appcompat:appcompat:$androidXAppCompatVersion" + } + + if (ns_engine == "HERMES") { + implementation 'com.facebook.fbjni:fbjni:0.7.0' + } + + def androidXMaterialVersion = "${ns_default_androidx_material_version}" + if (project.hasProperty("androidXMaterial")) { + androidXMaterialVersion = androidXMaterial + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library com.google.android.material:material:$androidXMaterialVersion" + } + + def androidXExifInterfaceVersion = "${ns_default_androidx_exifinterface_version}" + if (project.hasProperty("androidXExifInterface")) { + androidXExifInterfaceVersion = androidXExifInterface + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.exifinterface:exifinterface:$androidXExifInterfaceVersion" + } + + def androidXViewPagerVersion = "${ns_default_androidx_viewpager_version}" + if (project.hasProperty("androidXViewPager")) { + androidXViewPagerVersion = androidXViewPager + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.viewpager2:viewpager2:$androidXViewPagerVersion" + } + + def androidXFragmentVersion = "${ns_default_androidx_fragment_version}" + if (project.hasProperty("androidXFragment")) { + androidXFragmentVersion = androidXFragment + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.fragment:fragment:$androidXFragmentVersion" + } + + def androidXTransitionVersion = "${ns_default_androidx_transition_version}" + if (project.hasProperty("androidXTransition")) { + androidXTransitionVersion = androidXTransition + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.transition:transition:$androidXTransitionVersion" + } + + def androidXMultidexVersion = "${ns_default_androidx_multidex_version}" + if (project.hasProperty("androidXMultidex")) { + androidXMultidexVersion = androidXMultidex + outLogger.withStyle(Style.SuccessHeader).println "\t + using android X library androidx.multidex:multidex:$androidXMultidexVersion" + } + + implementation "androidx.multidex:multidex:$androidXMultidexVersion" + implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion" + debugImplementation "com.google.android.material:material:$androidXMaterialVersion" + implementation "androidx.exifinterface:exifinterface:$androidXExifInterfaceVersion" + implementation "androidx.viewpager2:viewpager2:$androidXViewPagerVersion" + //noinspection KtxExtensionAvailable + implementation "androidx.fragment:fragment:$androidXFragmentVersion" + implementation "androidx.transition:transition:$androidXTransitionVersion" + + def useV8Symbols = false + + def appPackageJsonFile = file("${getAppPath()}/$PACKAGE_JSON") + if (appPackageJsonFile.exists()) { + def appPackageJson = new JsonSlurper().parseText(appPackageJsonFile.text) + useV8Symbols = appPackageJson.android && appPackageJson.android.useV8Symbols + } + + if (!useV8Symbols) { + // check whether any of the dependencies require v8 symbols + useV8Symbols = nativescriptDependencies.any { + def packageJsonFile = file("$rootDir/${it.directory}/$PACKAGE_JSON") + def packageJson = new JsonSlurper().parseText(packageJsonFile.text) + return packageJson.nativescript && packageJson.nativescript.useV8Symbols + } + } + + if (!externalRuntimeExists) { + def runtime = "nativescript-optimized-with-inspector" + + if (project.gradle.startParameter.taskNames.any { it.toLowerCase().contains('release') }) { + runtime = "nativescript-optimized" + } + + if (useV8Symbols) { + runtime = "nativescript-regular" + } + + outLogger.withStyle(Style.SuccessHeader).println "\t + adding nativescript runtime package dependency: $runtime" + project.dependencies.add("implementation", [name: runtime, ext: "aar"]) + } else { + implementation project(':runtime') + } + +} + +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// CONFIGURATION PHASE ////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +task 'addDependenciesFromNativeScriptPlugins' { + nativescriptDependencies.each { dep -> + def aarFiles = fileTree(dir: file("$rootDir/${dep.directory}/$PLATFORMS_ANDROID"), include: ["**/*.aar"]) + aarFiles.each { aarFile -> + def length = aarFile.name.length() - 4 + def fileName = aarFile.name[0.. + def jarFileAbsolutePath = jarFile.getAbsolutePath() + outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" + pluginsJarLibraries.add(jarFile.getAbsolutePath()) + } + + project.dependencies.add("implementation", jarFiles) + } +} + +task 'addDependenciesFromAppResourcesLibraries' { + def appResourcesPath = getAppResourcesPath() + def appResourcesLibraries = file("$appResourcesPath/Android/libs") + if (appResourcesLibraries.exists()) { + def aarFiles = fileTree(dir: appResourcesLibraries, include: ["**/*.aar"]) + aarFiles.each { aarFile -> + def length = aarFile.name.length() - 4 + def fileName = aarFile.name[0.. + def jarFileAbsolutePath = jarFile.getAbsolutePath() + outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" + pluginsJarLibraries.add(jarFile.getAbsolutePath()) + } + + project.dependencies.add("implementation", jarFiles) + } +} + +if (failOnCompilationWarningsEnabled()) { + tasks.withType(JavaCompile).configureEach { + options.compilerArgs << '-Xlint:all' << "-Werror" + options.deprecation = true + } +} + + +//////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// EXECUTION PHASE ///////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +task runSbg(type: BuildToolTask) { + dependsOn "collectAllJars" + def rootPath = "" + if (!findProject(':static-binding-generator').is(null)) { + rootPath = Paths.get(project(':static-binding-generator').projectDir.path, "build/libs").toString() + dependsOn ':static-binding-generator:jar' + } + + outputs.dir("$OUTPUT_JAVA_DIR/com/tns/gen") + inputs.dir(INPUT_JS_DIR) + inputs.dir(extractedDependenciesDir) + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + def paramz = new ArrayList() + paramz.add(Paths.get(rootPath, "static-binding-generator.jar")) + + if (failOnCompilationWarningsEnabled()) { + paramz.add("-show-deprecation-warnings") + } + + if (ns_engine == "PRIMJS") { + paramz.add("-line-column-primjs") + } + + setOutputs outLogger + + args paramz + + doFirst { + new File("$OUTPUT_JAVA_DIR/com/tns/gen").deleteDir() + } +} + +def failOnCompilationWarningsEnabled() { + return project.hasProperty("failOnCompilationWarnings") && (failOnCompilationWarnings || failOnCompilationWarnings.toBoolean()) +} + +def explodeAar(File compileDependency, File outputDir) { + logger.info("explodeAar: Extracting ${compileDependency.path} -> ${outputDir.path}") + + if (compileDependency.name.endsWith(".aar")) { + JarFile jar = new JarFile(compileDependency) + Enumeration enumEntries = jar.entries() + while (enumEntries.hasMoreElements()) { + JarEntry file = (JarEntry) enumEntries.nextElement() + if (file.isDirectory()) { + continue + } + if (file.name.endsWith(".jar")) { + def targetFile = new File(outputDir, file.name) + InputStream inputStream = jar.getInputStream(file) + new File(targetFile.parent).mkdirs() + Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + jar.close() + } else if (compileDependency.name.endsWith(".jar")) { + copy { + from compileDependency.absolutePath + into outputDir + } + } +} + +static def md5(String string) { + MessageDigest digest = MessageDigest.getInstance("MD5") + digest.update(string.bytes) + return new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') +} + +class WorkerTask extends DefaultTask { + @Inject + WorkerExecutor getWorkerExecutor() { + throw new UnsupportedOperationException() + } +} + +class EmptyRunnable implements Runnable { + void run() { + } +} + +def getMergedAssetsOutputPath() { + if (!project.hasProperty("mergedAssetsOutputPath")) { + // mergedAssetsOutputPath not found fallback to the default value for android gradle plugin 3.5.1 + project.ext.mergedAssetsOutputPath = "$projectDir/build/intermediates/merged_assets/" + project.selectedBuildType + "/out" + } + return project.ext.mergedAssetsOutputPath +} + +// Discover all jars and dynamically create tasks for the extraction of each of them +project.ext.allJars = [] +allprojects { + afterEvaluate { project -> + def buildType = project.selectedBuildType + def jars = [] + def artifactType = Attribute.of('artifactType', String) + android.applicationVariants.configureEach { variant -> + if (variant.buildType.name == buildType) { + variant.getCompileClasspath(null).each { fileDependency -> + processJar(fileDependency, jars) + } + } + } + } +} + +def processJar(File jar, jars) { + if (!jars.contains(jar)) { + jars.add(jar) + def destDir = md5(jar.path) + def outputDir = new File(Paths.get(extractedDependenciesDir, destDir).normalize().toString()) + + def taskName = "extract_${jar.name}_to_${destDir}" + logger.debug("Creating dynamic task ${taskName}") + + // Add discovered jars as dependencies of cleanupAllJars. + // This is crucial for cloud builds because they are different + // on each incremental build (as each time the gradle user home + // directory is a randomly generated string) + cleanupAllJars.inputs.files jar + + task "${taskName}"(type: WorkerTask) { + dependsOn cleanupAllJars + extractAllJars.dependsOn it + + // This dependency seems redundant but probably due to some Gradle issue with workers, + // without it `runSbg` sporadically starts before all extraction tasks have finished and + // fails due to missing JARs + runSbg.dependsOn it + + inputs.files jar + outputs.dir outputDir + + doLast { + // Runing in parallel no longer seems to bring any benefit. + // It mattered only when we were extracting JARs from AARs. + // To try it simply remove the following comments. + // workerExecutor.submit(EmptyRunnable.class) { + explodeAar(jar, outputDir) + // } + } + } + project.ext.allJars.add([file: jar, outputDir: outputDir]) + } +} + +task 'cleanupAllJars' { + // We depend on the list of libs directories that might contain aar or jar files + // and on the list of all discovered jars + inputs.files(pluginDependencies) + + outputs.files cleanupAllJarsTimestamp + + doLast { + def allDests = project.ext.allJars*.outputDir*.name + def dir = new File(extractedDependenciesDir) + if (dir.exists()) { + dir.eachDir { + // An old directory which is no longer a dependency (e.g. orphaned by a deleted plugin) + if (!allDests.contains(it.name)) { + logger.info("Task cleanupAllJars: Deleting orphaned ${it.path}") + FileUtils.deleteDirectory(it) + } + } + } + new File(cleanupAllJarsTimestamp).write "" + } +} + + +// Placeholder task which depends on all dynamically generated extraction tasks +task 'extractAllJars' { + dependsOn cleanupAllJars + outputs.files extractAllJarsTimestamp + + doLast { + new File(cleanupAllJarsTimestamp).write "" + } +} + +task 'collectAllJars' { + dependsOn extractAllJars + description "gathers all paths to jar dependencies before building metadata with them" + + def sdkPath = android.sdkDirectory.getAbsolutePath() + def androidJar = sdkPath + "/platforms/" + android.compileSdkVersion + "/android.jar" + + doFirst { + def allJarPaths = new LinkedList() + allJarPaths.add(androidJar) + allJarPaths.addAll(pluginsJarLibraries) + def ft = fileTree(dir: extractedDependenciesDir, include: "**/*.jar") + ft.each { currentJarFile -> + allJarPaths.add(currentJarFile.getAbsolutePath()) + } + + new File("$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES").withWriter { out -> + allJarPaths.each { out.println it } + } + new File("$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES").withWriter { out -> + allJarPaths.each { + if (it.endsWith(".jar")) { + out.println it + } + } + } + + new File("$BUILD_TOOLS_PATH/$SBG_INPUT_FILE").withWriter { out -> + out.println INPUT_JS_DIR + } + new File("$BUILD_TOOLS_PATH/$SBG_OUTPUT_FILE").withWriter { out -> + out.println OUTPUT_JAVA_DIR + } + + allJarLibraries.addAll(allJarPaths) + } +} + +task copyMetadataFilters { + outputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + // use an explicit copy task here because the copy task itselfs marks the whole built-tools as an output! + copy { + from file("$rootDir/whitelist.mdg"), file("$rootDir/blacklist.mdg") + into "$BUILD_TOOLS_PATH" + } +} + +task 'copyMetadata' { + doLast { + copy { + from "$projectDir/src/main/assets/metadata" + into getMergedAssetsOutputPath() + "/metadata" + } + } +} + +def listf(String directoryName, ArrayList store) { + def directory = new File(directoryName) + + def resultList = new ArrayList() + + def fList = directory.listFiles() + resultList.addAll(Arrays.asList(fList)) + for (File file : fList) { + if (file.isFile()) { + store.add(file) + } else if (file.isDirectory()) { + resultList.addAll(listf(file.getAbsolutePath(), store)) + } + } + return resultList +} + +task buildMetadata(type: BuildToolTask) { + def rootPath = "" + if (!findProject(':android-metadata-generator').is(null)) { + rootPath = Paths.get(project(':android-metadata-generator').projectDir.path, "build/libs").toString() + dependsOn ':android-metadata-generator:jar' + } + + + android.applicationVariants.all { variant -> + def buildTypeName = variant.buildType.name.capitalize() + def mergeShadersTaskName = "merge${buildTypeName}Shaders" + def mergeShadersTask = tasks.findByName(mergeShadersTaskName) + + if (mergeShadersTask) { + dependsOn mergeShadersTask + } + + def compileJavaWithJavacTaskName = "compile${buildTypeName}JavaWithJavac" + def compileJavaWithJavacTask = tasks.findByName(compileJavaWithJavacTaskName) + + + if (compileJavaWithJavacTask) { + dependsOn compileJavaWithJavacTask + } + + def compileKotlinTaskName = "compile${buildTypeName}Kotlin" + def compileKotlinTask = tasks.findByName(compileKotlinTaskName) + + + if (compileKotlinTask) { + dependsOn compileKotlinTask + } + + + def mergeDexTaskName = "mergeDex${buildTypeName}" + def mergeDexTask = tasks.findByName(mergeDexTaskName) + + if (mergeDexTask) { + dependsOn mergeDexTask + } + + def checkDuplicateClassesTaskName = "check${buildTypeName}DuplicateClasses" + def checkDuplicateClassesTask = tasks.findByName(checkDuplicateClassesTaskName) + + if (checkDuplicateClassesTask) { + dependsOn checkDuplicateClassesTask + } + + def generateBuildConfigTaskName = "generate${buildTypeName}BuildConfig" + def generateBuildConfigTask = tasks.findByName(generateBuildConfigTaskName) + + if (generateBuildConfigTask) { + dependsOn generateBuildConfigTask + } + + def dexBuilderTaskName = "dexBuilder${buildTypeName}" + def dexBuilderTask = tasks.findByName(dexBuilderTaskName) + + if (dexBuilderTask) { + dependsOn dexBuilderTask + } + + + def mergeExtDexTaskName = "mergeExtDex${buildTypeName}" + def mergeExtDexTask = tasks.findByName(mergeExtDexTaskName) + + if (mergeExtDexTask) { + dependsOn mergeExtDexTask + } + + def mergeLibDexTaskName = "mergeLibDex${buildTypeName}" + def mergeLibDexTask = tasks.findByName(mergeLibDexTaskName) + + if (mergeLibDexTask) { + dependsOn mergeLibDexTask + } + + def mergeProjectDexTaskName = "mergeProjectDex${buildTypeName}" + def mergeProjectDexTask = tasks.findByName(mergeProjectDexTaskName) + + if (mergeProjectDexTask) { + dependsOn mergeProjectDexTask + } + + def syncLibJarsTaskName = "sync${buildTypeName}LibJars" + def syncLibJarsTask = tasks.findByName(syncLibJarsTaskName) + + if (syncLibJarsTask) { + dependsOn syncLibJarsTask + } + + def mergeJavaResourceTaskName = "merge${buildTypeName}JavaResource" + def mergeJavaResourceTask = tasks.findByName(mergeJavaResourceTaskName) + + if (mergeJavaResourceTask) { + dependsOn mergeJavaResourceTask + } + + def mergeJniLibFoldersTaskName = "merge${buildTypeName}JniLibFolders" + def mergeJniLibFoldersTask = tasks.findByName(mergeJniLibFoldersTaskName) + + if (mergeJniLibFoldersTask) { + dependsOn mergeJniLibFoldersTask + } + + def mergeNativeLibsTaskName = "merge${buildTypeName}NativeLibs" + def mergeNativeLibsTask = tasks.findByName(mergeNativeLibsTaskName) + + if (mergeNativeLibsTask) { + dependsOn mergeNativeLibsTask + } + + def stripDebugSymbolsTaskName = "strip${buildTypeName}DebugSymbols" + def stripDebugSymbolsTask = tasks.findByName(stripDebugSymbolsTaskName) + + if (stripDebugSymbolsTask) { + dependsOn stripDebugSymbolsTask + } + + def validateSigningTaskName = "validateSigning${buildTypeName}" + def validateSigningTask = tasks.findByName(validateSigningTaskName) + + if (validateSigningTask) { + dependsOn validateSigningTask + } + + + def extractProguardFilesTaskName = "extractProguardFiles" + def extractProguardFilesTask = tasks.findByName(extractProguardFilesTaskName) + + if (extractProguardFilesTask) { + dependsOn extractProguardFilesTask + } + + + def compileArtProfileTaskName = "compile${buildTypeName}ArtProfile" + def compileArtProfileTask = tasks.findByName(compileArtProfileTaskName) + + if (compileArtProfileTask) { + dependsOn compileArtProfileTask + } + + + def extractNativeSymbolTablesTaskName = "extract${buildTypeName}NativeSymbolTables" + def extractNativeSymbolTablesTask = tasks.findByName(extractNativeSymbolTablesTaskName) + + if (extractNativeSymbolTablesTask) { + dependsOn extractNativeSymbolTablesTask + } + + + def optimizeResourcesTaskName = "optimize${buildTypeName}Resources" + def optimizeResourcesTask = tasks.findByName(optimizeResourcesTaskName) + + if (optimizeResourcesTask) { + dependsOn optimizeResourcesTask + } + + def bundleResourcesTaskName = "bundle${buildTypeName}Resources" + def bundleResourcesTask = tasks.findByName(bundleResourcesTaskName) + + if (bundleResourcesTask) { + dependsOn bundleResourcesTask + } + + } + + dependsOn copyMetadataFilters + + // As some external gradle plugins can reorder the execution order of the tasks it may happen that buildMetadata is executed after merge{Debug/Release}Assets + // in that case the metadata won't be included in the result apk and it will crash, so to avoid this we are adding the copyMetadata task which will manually copy + // the metadata files in the merge assets folder and they will be added to the result apk + + // The next line is added to avoid adding another copyData implementation from the firebase plugin - https://github.com/EddyVerbruggen/nativescript-plugin-firebase/blob/3943bb9147f43c41599e801d026378eba93d3f3a/publish/scripts/installer.js#L1105 + //buildMetadata.finalizedBy(copyMetadata) + finalizedBy copyMetadata + + description "builds metadata with provided jar dependencies" + + inputs.files("$MDG_JAVA_DEPENDENCIES") + + // make MDG aware of whitelist.mdg and blacklist.mdg files + // inputs.files(project.fileTree(dir: "$rootDir", include: "**/*.mdg")) + // use explicit inputs as the above makes the whole build-tools directory an input! + inputs.files("$BUILD_TOOLS_PATH/whitelist.mdg", "$BUILD_TOOLS_PATH/blacklist.mdg") + + def classesDir = layout.buildDirectory.dir("intermediates/javac").get().asFile + if (classesDir.exists()) { + inputs.dir(classesDir) + } + + def kotlinClassesDir = layout.buildDirectory.dir("tmp/kotlin-classes").get().asFile + if (kotlinClassesDir.exists()) { + inputs.dir(kotlinClassesDir) + } + + outputs.files("$METADATA_OUT_PATH/treeNodeStream.dat", "$METADATA_OUT_PATH/treeStringsStream.dat", "$METADATA_OUT_PATH/treeValueStream.dat") + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + doFirst { + // get compiled classes to pass to metadata generator + // these need to be called after the classes have compiled + new File(getMergedAssetsOutputPath() + "/metadata").deleteDir() + + def classesSubDirs = [] + def kotlinClassesSubDirs = [] + def selectedBuildType = project.ext.selectedBuildType + + rootProject.subprojects { + + def projectClassesDir = it.layout.buildDirectory.dir("intermediates/javac").get().asFile + def projectKotlinClassesDir = it.layout.buildDirectory.dir("tmp/kotlin-classes").get().asFile + + if (projectClassesDir.exists()) { + def projectClassesSubDirs = projectClassesDir.listFiles() + for (File subDir : projectClassesSubDirs) { + if (!classesSubDirs.contains(subDir)) { + classesSubDirs.add(subDir) + } + } + } + + if (projectKotlinClassesDir.exists()) { + def projectKotlinClassesSubDirs = projectKotlinClassesDir.listFiles() + for (File subDir : projectKotlinClassesSubDirs) { + if (!kotlinClassesSubDirs.contains(subDir)) { + kotlinClassesSubDirs.add(subDir) + } + } + } + } + + def generatedClasses = new LinkedList() + for (File subDir : classesSubDirs) { + if (subDir.getName() == selectedBuildType) { + generatedClasses.add(subDir.getAbsolutePath()) + } + } + + for (File subDir : kotlinClassesSubDirs) { + if (subDir.getName() == selectedBuildType) { + generatedClasses.add(subDir.getAbsolutePath()) + } + } + + def store = new ArrayList() + for (String dir : generatedClasses) { + listf(dir, store) + } + + + new File("$BUILD_TOOLS_PATH/$METADATA_JAVA_OUT").withWriter { out -> + store.each { + out.println it.absolutePath + } + } + + + new File("$BUILD_TOOLS_PATH/$MDG_OUTPUT_DIR").withWriter { out -> + out.println "$METADATA_OUT_PATH" + } + + new File("$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES").withWriterAppend { out -> + generatedClasses.each { out.println it } + } + + setOutputs outLogger + + def paramz = new ArrayList() + paramz.add(Paths.get(rootPath, "android-metadata-generator.jar")) + + if (enableAnalytics) { + paramz.add("analyticsFilePath=$analyticsFilePath") + } + + if (enableVerboseMDG) { + paramz.add("verbose") + } + + args paramz.toArray() + } +} + +task generateTypescriptDefinitions(type: BuildToolTask) { + if (!findProject(':dts-generator').is(null)) { + dependsOn ':dts-generator:jar' + } + + def paramz = new ArrayList() + def includeDirs = ["com.android.support", "/platforms/" + android.compileSdkVersion] + + workingDir "$BUILD_TOOLS_PATH" + mainClass = "-jar" + + doFirst { + delete "$TYPINGS_PATH" + + paramz.add("dts-generator.jar") + paramz.add("-input") + + for (String jarPath : allJarLibraries) { + // don't generate typings for runtime jars and classes + if (shouldIncludeDirForTypings(jarPath, includeDirs)) { + paramz.add(jarPath) + } + } + + paramz.add("-output") + paramz.add("$TYPINGS_PATH") + + new File("$TYPINGS_PATH").mkdirs() + + logger.info("Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '')) + outLogger.withStyle(Style.SuccessHeader).println "Task generateTypescriptDefinitions: Call dts-generator.jar with arguments: " + paramz.toString().replaceAll(',', '') + + setOutputs outLogger + + args paramz.toArray() + } +} + +generateTypescriptDefinitions.onlyIf { + (project.hasProperty("generateTypings") && Boolean.parseBoolean(project.generateTypings)) || PASSED_TYPINGS_PATH != null +} + +collectAllJars.finalizedBy(generateTypescriptDefinitions) + +static def shouldIncludeDirForTypings(path, includeDirs) { + for (String p : includeDirs) { + if (path.indexOf(p) > -1) { + return true + } + } + + return false +} + +task 'copyTypings' { + doLast { + outLogger.withStyle(Style.Info).println "Copied generated typings to application root level. Make sure to import android.d.ts in reference.d.ts" + + copy { + from "$TYPINGS_PATH" + into "$USER_PROJECT_ROOT" + } + } +} + +copyTypings.onlyIf { generateTypescriptDefinitions.didWork } +generateTypescriptDefinitions.finalizedBy(copyTypings) + +task 'validateAppIdMatch' { + doLast { + def lineSeparator = System.getProperty("line.separator") + + if (project.hasProperty("nsApplicationIdentifier") && !project.hasProperty("release")) { + if (project.nsApplicationIdentifier != android.defaultConfig.applicationId && android.namespace != appIdentifier) { + def errorMessage = "${lineSeparator}WARNING: The Application identifier is different from the one inside \"package.json\" file.$lineSeparator" + + "NativeScript CLI might not work properly.$lineSeparator" + + "Remove applicationId from app.gradle and update the \"nativescript.id\" in package.json.$lineSeparator" + + "Actual: ${android.defaultConfig.applicationId}$lineSeparator" + + "Expected(from \"package.json\"): ${project.nsApplicationIdentifier}$lineSeparator" + + logger.error(errorMessage) + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////// +////////////////////////////// OPTIONAL TASKS ////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +//////// custom clean /////////// +task cleanSbg(type: Delete) { + delete "$BUILD_TOOLS_PATH/$SBG_JS_PARSED_FILES", + "$BUILD_TOOLS_PATH/$SBG_JAVA_DEPENDENCIES", + "$BUILD_TOOLS_PATH/$SBG_INTERFACE_NAMES", + "$BUILD_TOOLS_PATH/$SBG_BINDINGS_NAME", + "$BUILD_TOOLS_PATH/$SBG_INPUT_FILE", + "$BUILD_TOOLS_PATH/$SBG_OUTPUT_FILE", + "$BUILD_TOOLS_PATH/$METADATA_JAVA_OUT", + "$OUTPUT_JAVA_DIR/com/tns/gen" +} + +task cleanMdg(type: Delete) { + delete "$BUILD_TOOLS_PATH/$MDG_OUTPUT_DIR", + "$BUILD_TOOLS_PATH/whitelist.mdg", + "$BUILD_TOOLS_PATH/blacklist.mdg", + "$BUILD_TOOLS_PATH/$MDG_JAVA_DEPENDENCIES", + "$METADATA_OUT_PATH" +} + +cleanSbg.dependsOn(cleanMdg) +clean.dependsOn(cleanSbg) + + +//dependsOn { +// pattern { +// include "merge*.Shaders" // Matches tasks starting with "merge" and ending with "Shaders" +// } +//} + + +tasks.configureEach({ DefaultTask currentTask -> + // println "\t ~ [DEBUG][app] build.gradle - currentTask = ${currentTask.name} ..." + + if (currentTask =~ /compile.+JavaWithJavac/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /mergeDex.+/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /compile.+Kotlin.+/) { + currentTask.dependsOn(runSbg) + } + + if (currentTask =~ /merge.*Assets/) { + currentTask.dependsOn(buildMetadata) + } + +// // ensure buildMetadata is done before R8 to allow custom proguard from metadata + if (currentTask =~ /minify.*WithR8/) { + buildMetadata.finalizedBy(currentTask) + } + if (currentTask =~ /assemble.*Debug/ || currentTask =~ /assemble.*Release/) { + currentTask.finalizedBy("validateAppIdMatch") + } + + if (currentTask =~ /process.+Resources/) { + cleanupAllJars.dependsOn(currentTask) + } + +// if (currentTask.name == "extractProguardFiles") { +// currentTask.finalizedBy(buildMetadata) +// } +// + if (currentTask =~ /generate.+LintVitalReportModel/) { + currentTask.dependsOn(buildMetadata) + } + + if (currentTask =~ /lintVitalAnalyze.+/) { + currentTask.dependsOn(buildMetadata) + } +// +// if (currentTask =~ /merge.+GlobalSynthetics/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /optimize.+Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /buildCMake.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /configureCMake.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /validateSigning.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*LintReportModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*AndroidTestResValues/) { +// // buildMetadata.dependsOn(currentTask) +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*AndroidTestLintModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*UnitTestLintModel/) { +// buildMetadata.mustRunAfter(currentTask) +// } +// +// if (currentTask =~ /generate.*UnitTestLintModel/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// +// if (currentTask =~ /lintAnalyze.*UnitTest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*JavaRes/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /strip.*DebugSymbols/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*JavaResource/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /lintAnalyze.*/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /lintAnalyze.*AndroidTest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /bundle.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /compile.*ArtProfile/) { +// currentTask.mustRunAfter(buildMetadata) +// } +// +// if (currentTask =~ /check.*DuplicateClasses/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /check.*AarMetadata/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /create.*CompatibleScreenManifests/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*Manifest/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /generate.*ResValues/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /package.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /process.*Resources/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /desugar.*Dependencies/) { +// currentTask.finalizedBy(buildMetadata) +// } +// +// if (currentTask =~ /merge.*JniLibFolders/) { +// currentTask.finalizedBy(buildMetadata) +// } + +}) + +rootProject.subprojects.forEach { + it.tasks.configureEach({ DefaultTask currentTask -> + if (currentTask =~ /.+bundleLibCompileToJar.*/) { + currentTask.finalizedBy(cleanupAllJars) + } + + if (currentTask =~ /bundleLibRuntimeToDir.*/) { + currentTask.finalizedBy(buildMetadata) + } + + if (currentTask =~ /compile.*LibraryResources/) { + currentTask.finalizedBy(buildMetadata) + } + }) +} diff --git a/platforms/android/test-app/app/gradle-helpers/AnalyticsCollector.gradle b/platforms/android/test-app/app/gradle-helpers/AnalyticsCollector.gradle new file mode 100644 index 000000000..72fa363a7 --- /dev/null +++ b/platforms/android/test-app/app/gradle-helpers/AnalyticsCollector.gradle @@ -0,0 +1,48 @@ +import groovy.json.JsonBuilder + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.Path + +class AnalyticsCollector{ + + private final String analyticsFilePath + private boolean hasUseKotlinPropertyInApp = false + private boolean hasKotlinRuntimeClasses = false + + private AnalyticsCollector(String analyticsFilePath){ + this.analyticsFilePath = analyticsFilePath + } + + static AnalyticsCollector withOutputPath(String analyticsFilePath){ + return new AnalyticsCollector(analyticsFilePath) + } + + void markUseKotlinPropertyInApp(boolean useKotlin) { + hasUseKotlinPropertyInApp = useKotlin + } + + void writeAnalyticsFile() { + def jsonBuilder = new JsonBuilder() + def kotlinUsageData = new Object() + kotlinUsageData.metaClass.hasUseKotlinPropertyInApp = hasUseKotlinPropertyInApp + kotlinUsageData.metaClass.hasKotlinRuntimeClasses = hasKotlinRuntimeClasses + jsonBuilder(kotlinUsage: kotlinUsageData) + def prettyJson = jsonBuilder.toPrettyString() + + + + Path statisticsFilePath = Paths.get(analyticsFilePath) + + if (Files.notExists(statisticsFilePath)) { + Files.createDirectories(statisticsFilePath.getParent()) + Files.createFile(statisticsFilePath) + } + + Files.write(statisticsFilePath, prettyJson.getBytes(StandardCharsets.UTF_8)) + + } +} + +ext.AnalyticsCollector = AnalyticsCollector diff --git a/platforms/android/test-app/app/gradle-helpers/BuildToolTask.gradle b/platforms/android/test-app/app/gradle-helpers/BuildToolTask.gradle new file mode 100644 index 000000000..7b6052f4c --- /dev/null +++ b/platforms/android/test-app/app/gradle-helpers/BuildToolTask.gradle @@ -0,0 +1,50 @@ +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +class BuildToolTask extends JavaExec { + void setOutputs(def logger) { + def logFile = new File("$workingDir/${name}.log") + if(logFile.exists()) { + logFile.delete() + } + standardOutput new FileOutputStream(logFile) + errorOutput new FailureOutputStream(logger, logFile) + } +} + +class FailureOutputStream extends OutputStream { + private logger + private File logFile + private currentLine = "" + private firstWrite = true + FailureOutputStream(inLogger, inLogFile) { + logger = inLogger + logFile = inLogFile + } + + @Override + void write(int i) throws IOException { + if(firstWrite) { + println "" + firstWrite = false + } + currentLine += String.valueOf((char) i) + } + + @Override + void flush() { + if(currentLine?.trim()) { + logger.withStyle(Style.Failure).println currentLine.trim() + currentLine = "" + } + } + + @Override + void close() { + if(!firstWrite && logFile.exists()) { + logger.withStyle(Style.Info).println "Detailed log here: ${logFile.getAbsolutePath()}\n" + } + super.close() + } +} + +ext.BuildToolTask = BuildToolTask \ No newline at end of file diff --git a/platforms/android/test-app/app/gradle-helpers/CustomExecutionLogger.gradle b/platforms/android/test-app/app/gradle-helpers/CustomExecutionLogger.gradle new file mode 100644 index 000000000..ec8ea6c42 --- /dev/null +++ b/platforms/android/test-app/app/gradle-helpers/CustomExecutionLogger.gradle @@ -0,0 +1,52 @@ +import org.gradle.internal.logging.text.StyledTextOutputFactory + +import static org.gradle.internal.logging.text.StyledTextOutput.Style +def outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") + +class CustomExecutionLogger extends BuildAdapter implements TaskExecutionListener { + private logger + private failedTask + + CustomExecutionLogger(passedLogger) { + logger = passedLogger + } + + void buildStarted(Gradle gradle) { + failedTask = null + } + + void beforeExecute(Task task) { + } + + void afterExecute(Task task, TaskState state) { + def failure = state.getFailure() + if(failure) { + failedTask = task + } + } + + void buildFinished(BuildResult result) { + def failure = result.getFailure() + if(failure) { + if(failedTask && (failedTask.getClass().getName().contains("BuildToolTask"))) { + // the error from this task is already logged + return + } + + println "" + logger.withStyle(Style.FailureHeader).println failure.getMessage() + + def causeException = failure.getCause() + while (causeException != null) { + failure = causeException + causeException = failure.getCause() + } + if(failure != causeException) { + logger.withStyle(Style.Failure).println failure.getMessage() + } + println "" + } + } +} + +gradle.useLogger(new CustomExecutionLogger(outLogger)) \ No newline at end of file diff --git a/platforms/android/test-app/app/proguard-rules.pro b/platforms/android/test-app/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/platforms/android/test-app/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReport.java b/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReport.java new file mode 100644 index 000000000..d2b5cf2d6 --- /dev/null +++ b/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReport.java @@ -0,0 +1,512 @@ +package com.tns; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; + +import android.Manifest; +import android.app.Activity; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import com.google.android.material.tabs.TabLayout; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.ViewPager; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.method.LinkMovementMethod; +import android.text.method.ScrollingMovementMethod; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +class ErrorReport implements TabLayout.OnTabSelectedListener { + public static final String ERROR_FILE_NAME = "hasError"; + private static AppCompatActivity activity; + + private TabLayout tabLayout; + private ViewPager viewPager; + private Context context; + + private static String exceptionMsg; + private static String logcatMsg; + + private static boolean checkingForPermissions = false; + + private final static String EXTRA_NATIVESCRIPT_ERROR_REPORT = "NativeScriptErrorMessage"; + private final static String EXTRA_ERROR_REPORT_MSG = "msg"; + private final static String EXTRA_PID = "pID"; + private final static int EXTRA_ERROR_REPORT_VALUE = 1; + + private static final int REQUEST_EXTERNAL_STORAGE = 1; + private static String[] PERMISSIONS_STORAGE = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + + // Will prevent error activity from killing process if permission request dialog pops up + public static boolean isCheckingForPermissions() { + return checkingForPermissions; + } + + public static void resetCheckingForPermissions() { + checkingForPermissions = false; + } + + // The following will not compile if uncommented with compileSdk lower than 23 + public static void verifyStoragePermissions(Activity activity) { + // Check if we have write permission + final int version = Build.VERSION.SDK_INT; + if (version >= 23) { + try { + // Necessary to work around compile errors with compileSdk 22 and lower + Method checkSelfPermissionMethod; + try { + checkSelfPermissionMethod = ActivityCompat.class.getMethod("checkSelfPermission", Context.class, String.class); + } catch (NoSuchMethodException e) { + // method wasn't found, so there is no need to handle permissions explicitly + if (Util.isDebuggableApp(activity)) { + e.printStackTrace(); + } + return; + } + + int permission = (int) checkSelfPermissionMethod.invoke(null, activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + + if (permission != PackageManager.PERMISSION_GRANTED) { + // We don't have permission so prompt the user + Method requestPermissionsMethod = ActivityCompat.class.getMethod("requestPermissions", Activity.class, PERMISSIONS_STORAGE.getClass(), int.class); + + checkingForPermissions = true; + + requestPermissionsMethod.invoke(null, activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); + } + } catch (Exception e) { + Toast.makeText(activity, "Couldn't resolve permissions", Toast.LENGTH_LONG).show(); + if (Util.isDebuggableApp(activity)) { + e.printStackTrace(); + } + return; + } + } + } + + public ErrorReport(AppCompatActivity activity) { + ErrorReport.activity = activity; + this.context = activity.getApplicationContext(); + } + + static boolean startActivity(final Context context, String errorMessage) { + final Intent intent = getIntent(context); + if (intent == null) { + return false; // (if in release mode) don't do anything + } + + intent.putExtra(EXTRA_ERROR_REPORT_MSG, errorMessage); + + String PID = Integer.toString(android.os.Process.myPid()); + intent.putExtra(EXTRA_PID, PID); + + createErrorFile(context); + + try { + startPendingErrorActivity(context, intent); + } catch (CanceledException e) { + Log.d("ErrorReport", "Couldn't send pending intent! Exception: " + e.getMessage()); + } + + killProcess(context); + + return true; + } + + static void killProcess(Context context) { + // finish current activity and all below it first + if (context instanceof Activity) { + ((Activity) context).finishAffinity(); + } + + // kill process + android.os.Process.killProcess(android.os.Process.myPid()); + } + + static void startPendingErrorActivity(Context context, Intent intent) throws CanceledException { + int flags = PendingIntent.FLAG_CANCEL_CURRENT; + if (Build.VERSION.SDK_INT >= 31) { + flags = PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE; + } + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags); + + pendingIntent.send(context, 0, intent); + } + + static String getErrorMessage(Throwable ex) { + String content; + PrintStream ps = null; + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ps = new PrintStream(baos); + ex.printStackTrace(ps); + + try { + content = baos.toString("UTF-8"); + } catch (UnsupportedEncodingException e) { + content = e.getMessage(); + } + } finally { + if (ps != null) { + ps.close(); + } + } + + return content; + } + + /* + * Gets the process Id of the running app and filters all + * output that doesn't belong to that process + * */ + public static String getLogcat(String pId) { + String content; + + try { + String logcatCommand = "logcat -d"; + Process process = java.lang.Runtime.getRuntime().exec(logcatCommand); + + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(process.getInputStream())); + + StringBuilder log = new StringBuilder(); + String line = ""; + String lineSeparator = System.getProperty("line.separator"); + while ((line = bufferedReader.readLine()) != null) { + if (line.contains(pId)) { + log.append(line); + log.append(lineSeparator); + } + } + + content = log.toString(); + } catch (IOException e) { + content = "Failed to read logcat"; + Log.e("TNS.Android", content); + } + + return content; + } + + static Intent getIntent(Context context) { + Class errorActivityClass; + + if (Util.isDebuggableApp(context)) { + errorActivityClass = ErrorReportActivity.class; + } else { + return null; + } + + Intent intent = new Intent(context, errorActivityClass); + + intent.putExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, EXTRA_ERROR_REPORT_VALUE); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + + return intent; + } + + static boolean hasIntent(Intent intent) { + int value = intent.getIntExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, 0); + + return value == EXTRA_ERROR_REPORT_VALUE; + } + + void buildUI() { + Intent intent = activity.getIntent(); + + exceptionMsg = intent.getStringExtra(EXTRA_ERROR_REPORT_MSG); + + String processId = intent.getStringExtra(EXTRA_PID); + logcatMsg = getLogcat(processId); + + int errActivityId = this.context.getResources().getIdentifier("error_activity", "layout", this.context.getPackageName()); + + activity.setContentView(errActivityId); + + int toolBarId = this.context.getResources().getIdentifier("toolbar", "id", this.context.getPackageName()); + + Toolbar toolbar = (Toolbar) activity.findViewById(toolBarId); + activity.setSupportActionBar(toolbar); + + final int tabLayoutId = this.context.getResources().getIdentifier("tabLayout", "id", this.context.getPackageName()); + + tabLayout = (TabLayout) activity.findViewById(tabLayoutId); + tabLayout.addTab(tabLayout.newTab().setText("Exception")); + tabLayout.addTab(tabLayout.newTab().setText("Logcat")); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + int pagerId = this.context.getResources().getIdentifier("pager", "id", this.context.getPackageName()); + + viewPager = (ViewPager) activity.findViewById(pagerId); + + Pager adapter = new Pager(activity.getSupportFragmentManager(), tabLayout.getTabCount()); + + viewPager.setAdapter(adapter); + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + tabLayout.getTabAt(position).select(); + viewPager.setCurrentItem(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + + this.addOnTabSelectedListener(tabLayout); + } + + private void addOnTabSelectedListener(TabLayout tabLayout) { + tabLayout.addOnTabSelectedListener(this); + } + private static void createErrorFile(final Context context) { + try { + File errFile = new File(context.getFilesDir(), ERROR_FILE_NAME); + errFile.createNewFile(); + } catch (IOException e) { + Log.d("ErrorReport", e.getMessage()); + } + } + + @Override + public void onTabSelected(TabLayout.Tab tab) { + viewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + viewPager.setCurrentItem(tab.getPosition()); + } + + private class Pager extends FragmentStatePagerAdapter { + + int tabCount; + + @SuppressWarnings("deprecation") + public Pager(FragmentManager fm, int tabCount) { + super(fm); + this.tabCount = tabCount; + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: + return new ExceptionTab(); + case 1: + return new LogcatTab(); + default: + return null; + } + } + + @Override + public int getCount() { + return tabCount; + } + } + + + public static class ExceptionTab extends Fragment { + + public SpannableStringBuilder getStyledStacktrace(String trace) { + if (trace == null) return null; + String[] traceLines = trace.trim().split("\n"); + SpannableStringBuilder builder = new SpannableStringBuilder(); + boolean firstLine = true; + for (String line: traceLines) { + if (firstLine) { + firstLine = false; + } else { + builder.append("\n"); + builder.append("\n"); + } + + String[] nameAndPath = line.trim().split("\\("); + SpannableString nameSpan = new SpannableString(nameAndPath[0]); + nameSpan.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, nameAndPath[0].length(), 0); + builder.append(nameSpan); + + builder.append(" "); + if (nameAndPath.length > 1) { + SpannableString pathSpan = new SpannableString("(" + nameAndPath[1]); + pathSpan.setSpan(new AbsoluteSizeSpan(13, true),0, nameAndPath[1].length() + 1, 0); + pathSpan.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + Log.d("JS", line.trim()); + } + }, 0,nameAndPath[1].length() + 1, 0); + pathSpan.setSpan(new ForegroundColorSpan(Color.GRAY),0, nameAndPath[1].length() + 1, 0); + + builder.append(pathSpan); + } + } + return builder; + } + + public static void restartApp(Context context) { + PackageManager packageManager = context.getPackageManager(); + Intent intent = packageManager.getLaunchIntentForPackage(context.getPackageName()); + ComponentName componentName = intent.getComponent(); + Intent mainIntent = Intent.makeRestartActivityTask(componentName); + context.startActivity(mainIntent); + java.lang.Runtime.getRuntime().exit(0); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + int exceptionTabId = container.getContext().getResources().getIdentifier("exception_tab", "layout", container.getContext().getPackageName()); + View view = inflater.inflate(exceptionTabId, container, false); + + int errorExceptionViewId = activity.getResources().getIdentifier("errorException", "id", activity.getPackageName()); + TextView errorExceptionView = (TextView) activity.findViewById(errorExceptionViewId); + errorExceptionView.setMovementMethod(new ScrollingMovementMethod()); + + int errorStackTraceViewId = container.getContext().getResources().getIdentifier("errorStacktrace", "id", container.getContext().getPackageName()); + TextView errorStackTraceView = (TextView) view.findViewById(errorStackTraceViewId); + + String[] exceptionParts = exceptionMsg.split("StackTrace:"); + String error = exceptionParts[0]; + String trace = ""; + + if (exceptionParts.length > 1) { + for (int i=0;i < exceptionParts.length;i++) { + if (i == 0) continue; + trace += exceptionParts[i]; + } + } + + errorExceptionView.setText(error.trim()); + + errorStackTraceView.setText(trace != null ? getStyledStacktrace(trace) : "", TextView.BufferType.SPANNABLE); + errorStackTraceView.setMovementMethod(new ScrollingMovementMethod()); + errorStackTraceView.setMovementMethod(LinkMovementMethod.getInstance()); + errorStackTraceView.setEnabled(true); + + int btnCopyExceptionId = container.getContext().getResources().getIdentifier("btnCopyException", "id", container.getContext().getPackageName()); + Button copyToClipboard = (Button) view.findViewById(btnCopyExceptionId); + + int btnRestartAppId = container.getContext().getResources().getIdentifier("btnRestartApp", "id", container.getContext().getPackageName()); + Button restartApp = (Button) view.findViewById(btnRestartAppId); + restartApp.setOnClickListener(v -> { + restartApp(getContext().getApplicationContext()); + }); + copyToClipboard.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("nsError", exceptionMsg); + clipboard.setPrimaryClip(clip); + }); + + return view; + } + } + + public static class LogcatTab extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + int logcatTabId = container.getContext().getResources().getIdentifier("logcat_tab", "layout", container.getContext().getPackageName()); + View view = inflater.inflate(logcatTabId, container, false); + + int textViewId = container.getContext().getResources().getIdentifier("logcatMsg", "id", container.getContext().getPackageName()); + TextView txtlogcatMsg = (TextView) view.findViewById(textViewId); + txtlogcatMsg.setText(logcatMsg); + + txtlogcatMsg.setMovementMethod(new ScrollingMovementMethod()); + + final String logName = "Log-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); + + int btnCopyLogcatId = container.getContext().getResources().getIdentifier("btnCopyLogcat", "id", container.getContext().getPackageName()); + Button copyToClipboard = (Button) view.findViewById(btnCopyLogcatId); + copyToClipboard.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + verifyStoragePermissions(activity); + + if (!isCheckingForPermissions()) { + try { + File dir = new File(Environment.getExternalStorageDirectory().getPath() + "/logcat-reports/"); + dir.mkdirs(); + + File logcatReportFile = new File(dir, logName); + FileOutputStream stream = new FileOutputStream(logcatReportFile); + OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8"); + writer.write(logcatMsg); + writer.close(); + + String logPath = dir.getPath() + "/" + logName; + + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("logPath", logPath); + clipboard.setPrimaryClip(clip); + + Toast.makeText(activity, "Path copied to clipboard: " + logPath, Toast.LENGTH_LONG).show(); + } catch (Exception e) { + String err = "Could not write logcat report to sdcard. Make sure you have allowed access to external storage!"; + Toast.makeText(activity, err, Toast.LENGTH_LONG).show(); + if (Util.isDebuggableApp(container.getContext())) { + e.printStackTrace(); + } + } + } + } + }); + + return view; + } + } +} diff --git a/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReportActivity.java b/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReportActivity.java new file mode 100644 index 000000000..88a46658b --- /dev/null +++ b/platforms/android/test-app/app/src/debug/java/com/tns/ErrorReportActivity.java @@ -0,0 +1,52 @@ +package com.tns; + +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import android.widget.Toast; + +import java.lang.reflect.Method; + +import static com.tns.ErrorReport.isCheckingForPermissions; +import static com.tns.ErrorReport.resetCheckingForPermissions; + +public class ErrorReportActivity extends AppCompatActivity { + public void onCreate(Bundle savedInstanceState) { + setTheme(androidx.appcompat.R.style.Theme_AppCompat_NoActionBar); + + super.onCreate(savedInstanceState); + Application app = this.getApplication(); + Logger logger = new LogcatLogger(app); + + RuntimeHelper.initLiveSync(null, logger, app); + + new ErrorReport(this).buildUI(); + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + + if (!isCheckingForPermissions()) { + ErrorReport.killProcess(this); + } + } + + // @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + try { + Method onRequestPermissionsResultMethod = AppCompatActivity.class.getMethod("onRequestPermissionsResult", int.class, permissions.getClass(), grantResults.getClass()); + onRequestPermissionsResultMethod.invoke(new AppCompatActivity() /* never do this */, requestCode, permissions, grantResults); + + resetCheckingForPermissions(); + } catch (Exception e) { + if (Util.isDebuggableApp(this)) { + e.printStackTrace(); + } + Toast.makeText(this, "Couldn't resolve permissions", Toast.LENGTH_LONG).show(); + resetCheckingForPermissions(); + } + } +} diff --git a/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncService.java b/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncService.java new file mode 100644 index 000000000..5778025fa --- /dev/null +++ b/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncService.java @@ -0,0 +1,346 @@ +package com.tns; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.util.Log; + +public class NativeScriptSyncService { + private static String SYNC_ROOT_SOURCE_DIR = "/data/local/tmp/"; + private static final String SYNC_SOURCE_DIR = "/sync/"; + private static final String FULL_SYNC_SOURCE_DIR = "/fullsync/"; + private static final String REMOVED_SYNC_SOURCE_DIR = "/removedsync/"; + + private final Runtime runtime; + private static Logger logger; + private final Context context; + + private final String syncPath; + private final String fullSyncPath; + private final String removedSyncPath; + private final File fullSyncDir; + private final File syncDir; + private final File removedSyncDir; + + private LocalServerSocketThread localServerThread; + private Thread localServerJavaThread; + + public NativeScriptSyncService(Runtime runtime, Logger logger, Context context) { + this.runtime = runtime; + NativeScriptSyncService.logger = logger; + this.context = context; + + syncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + SYNC_SOURCE_DIR; + fullSyncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + FULL_SYNC_SOURCE_DIR; + removedSyncPath = SYNC_ROOT_SOURCE_DIR + context.getPackageName() + REMOVED_SYNC_SOURCE_DIR; + fullSyncDir = new File(fullSyncPath); + syncDir = new File(syncPath); + removedSyncDir = new File(removedSyncPath); + } + + public void sync() { + if (logger != null && logger.isEnabled()) { + logger.write("Sync is enabled:"); + logger.write("Sync path : " + syncPath); + logger.write("Full sync path : " + fullSyncPath); + logger.write("Removed files sync path: " + removedSyncPath); + } + + if (fullSyncDir.exists()) { + executeFullSync(context, fullSyncDir); + return; + } + + if (syncDir.exists()) { + executePartialSync(context, syncDir); + } + + if (removedSyncDir.exists()) { + executeRemovedSync(context, removedSyncDir); + } + } + + private class LocalServerSocketThread implements Runnable { + private volatile boolean running; + private final String name; + + private ListenerWorker commThread; + private LocalServerSocket serverSocket; + + public LocalServerSocketThread(String name) { + this.name = name; + this.running = false; + } + + public void stop() { + this.running = false; + try { + serverSocket.close(); + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + + public void run() { + running = true; + try { + serverSocket = new LocalServerSocket(this.name); + while (running) { + LocalSocket socket = serverSocket.accept(); + commThread = new ListenerWorker(socket); + new Thread(commThread).start(); + } + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + } + + private class ListenerWorker implements Runnable { + private final DataInputStream input; + private Closeable socket; + private OutputStream output; + + public ListenerWorker(LocalSocket socket) throws IOException { + this.socket = socket; + input = new DataInputStream(socket.getInputStream()); + output = socket.getOutputStream(); + } + + public void run() { + try { + int length = input.readInt(); + input.readFully(new byte[length]); // ignore the payload + executePartialSync(context, syncDir); + executeRemovedSync(context, removedSyncDir); + + runtime.runScript(new File(NativeScriptSyncService.this.context.getFilesDir(), "internal/livesync.js")); + try { + output.write(1); + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + socket.close(); + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + } + + public void startServer() { + localServerThread = new LocalServerSocketThread(context.getPackageName() + "-livesync"); + localServerJavaThread = new Thread(localServerThread); + localServerJavaThread.start(); + } + + public static boolean isSyncEnabled(Context context) { + int flags; + boolean shouldExecuteSync = false; + try { + flags = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.flags; + shouldExecuteSync = ((flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0); + } catch (NameNotFoundException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + return false; + } + + return shouldExecuteSync; + } + + final FileFilter deletingFilesFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + if (pathname.isDirectory()) { + return true; + } + + boolean success = pathname.delete(); + if (!success) { + logger.write("Syncing: file not deleted: " + pathname.getAbsolutePath().toString()); + } + return false; + } + }; + + private void deleteDir(File directory) { + File[] subDirectories = directory.listFiles(deletingFilesFilter); + if (subDirectories != null) { + for (int i = 0; i < subDirectories.length; i++) { + File subDir = subDirectories[i]; + deleteDir(subDir); + } + } + + boolean success = directory.delete(); + if (!success && directory.exists()) { + logger.write("Syncing: directory not deleted: " + directory.getAbsolutePath().toString()); + } + } + + private void moveFiles(File sourceDir, String sourceRootAbsolutePath, String targetRootAbsolutePath) { + File[] files = sourceDir.listFiles(); + + if (files != null) { + if (logger.isEnabled()) { + logger.write("Syncing total number of fiiles: " + files.length); + } + + for (int i = 0; i < files.length; i++) { + File file = files[i]; + if (file.isFile()) { + if (logger.isEnabled()) { + logger.write("Syncing: " + file.getAbsolutePath().toString()); + } + + String targetFilePath = file.getAbsolutePath().replace(sourceRootAbsolutePath, targetRootAbsolutePath); + File targetFileDir = new File(targetFilePath); + + File targetParent = targetFileDir.getParentFile(); + if (targetParent != null) { + targetParent.mkdirs(); + } + + boolean success = copyFile(file.getAbsolutePath(), targetFilePath); + if (!success) { + logger.write("Sync failed: " + file.getAbsolutePath().toString()); + } + } else { + moveFiles(file, sourceRootAbsolutePath, targetRootAbsolutePath); + } + } + } else { + if (logger.isEnabled()) { + logger.write("Can't move files. Source is empty."); + } + } + } + + // this removes only the app directory from the device to preserve + // any existing files in /files directory on the device + private void executeFullSync(Context context, final File sourceDir) { + String appPath = context.getFilesDir().getAbsolutePath() + "/app"; + final File appDir = new File(appPath); + + if (appDir.exists()) { + deleteDir(appDir); + moveFiles(sourceDir, sourceDir.getAbsolutePath(), appDir.getAbsolutePath()); + } + } + + private void executePartialSync(Context context, File sourceDir) { + String appPath = context.getFilesDir().getAbsolutePath() + "/app"; + final File appDir = new File(appPath); + + if (!appDir.exists()) { + Log.e("TNS", "Application dir does not exists. Partial Sync failed. appDir: " + appPath); + return; + } + + if (logger.isEnabled()) { + logger.write("Syncing sourceDir " + sourceDir.getAbsolutePath() + " with " + appDir.getAbsolutePath()); + } + + moveFiles(sourceDir, sourceDir.getAbsolutePath(), appDir.getAbsolutePath()); + } + + private void deleteRemovedFiles(File sourceDir, String sourceRootAbsolutePath, String targetRootAbsolutePath) { + if (!sourceDir.exists()) { + if (logger.isEnabled()) { + logger.write("Directory does not exist: " + sourceDir.getAbsolutePath()); + } + } + + File[] files = sourceDir.listFiles(); + + if (files != null) { + for (int i = 0; i < files.length; i++) { + File file = files[i]; + String targetFilePath = file.getAbsolutePath().replace(sourceRootAbsolutePath, targetRootAbsolutePath); + File targetFile = new File(targetFilePath); + if (file.isFile()) { + if (logger.isEnabled()) { + logger.write("Syncing removed file: " + file.getAbsolutePath().toString()); + } + + targetFile.delete(); + } else { + deleteRemovedFiles(file, sourceRootAbsolutePath, targetRootAbsolutePath); + + // this is done so empty folders, if any, are deleted after we're don deleting files. + if (targetFile.listFiles().length == 0) { + targetFile.delete(); + } + } + } + } + } + + private void executeRemovedSync(final Context context, final File sourceDir) { + String appPath = context.getFilesDir().getAbsolutePath() + "/app"; + deleteRemovedFiles(sourceDir, sourceDir.getAbsolutePath(), appPath); + } + + private boolean copyFile(String sourceFile, String destinationFile) { + FileInputStream fis = null; + FileOutputStream fos = null; + + try { + fis = new FileInputStream(sourceFile); + fos = new FileOutputStream(destinationFile, false); + + byte[] buffer = new byte[4096]; + int read = 0; + + while ((read = fis.read(buffer)) != -1) { + fos.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + logger.write("Error copying file " + sourceFile); + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + return false; + } catch (IOException e) { + logger.write("Error copying file " + sourceFile); + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + return false; + } finally { + try { + if (fis != null) { + fis.close(); + } + if (fos != null) { + fos.close(); + } + } catch (IOException e) { + } + } + + return true; + } +} \ No newline at end of file diff --git a/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncServiceSocketImpl.java b/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncServiceSocketImpl.java new file mode 100644 index 000000000..989a8a86e --- /dev/null +++ b/platforms/android/test-app/app/src/debug/java/com/tns/NativeScriptSyncServiceSocketImpl.java @@ -0,0 +1,448 @@ +package com.tns; + +import android.content.Context; +import android.net.LocalServerSocket; +import android.net.LocalSocket; + +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.io.OutputStream; +import java.util.Arrays; + +public class NativeScriptSyncServiceSocketImpl { + private static String DEVICE_APP_DIR; + + private final Runtime runtime; + private static Logger logger; + private final Context context; + + private LocalServerSocketThread localServerThread; + private Thread localServerJavaThread; + + public NativeScriptSyncServiceSocketImpl(Runtime runtime, Logger logger, Context context) { + this.runtime = runtime; + NativeScriptSyncServiceSocketImpl.logger = logger; + this.context = context; + DEVICE_APP_DIR = this.context.getFilesDir().getAbsolutePath() + "/app"; + } + + private class LocalServerSocketThread implements Runnable { + + private volatile boolean running; + private final String name; + + private LiveSyncWorker livesyncWorker; + private LocalServerSocket deviceSystemSocket; + + public LocalServerSocketThread(String name) { + this.name = name; + this.running = false; + } + + public void stop() { + this.running = false; + try { + deviceSystemSocket.close(); + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + + public void run() { + running = true; + try { + deviceSystemSocket = new LocalServerSocket(this.name); + while (running) { + LocalSocket systemSocket = deviceSystemSocket.accept(); + livesyncWorker = new LiveSyncWorker(systemSocket); + Thread liveSyncThread = setUpLivesyncThread(); + liveSyncThread.start(); + } + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + catch (java.security.NoSuchAlgorithmException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + + private Thread setUpLivesyncThread() { + Thread livesyncThread = new Thread(livesyncWorker); + livesyncThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + logger.write(String.format("%s(%s): %s", t.getName(), t.getId(), e.toString())); + } + }); + livesyncThread.setName("Livesync Thread"); + return livesyncThread; + } + + @Override + protected void finalize() throws Throwable { + deviceSystemSocket.close(); + } + } + + public void startServer() { + localServerThread = new LocalServerSocketThread(context.getPackageName() + "-livesync"); + localServerJavaThread = new Thread(localServerThread); + localServerJavaThread.setName("Livesync Server Thread"); + localServerJavaThread.start(); + } + + private class LiveSyncWorker implements Runnable { + public static final int OPERATION_BYTE_SIZE = 1; + public static final int HASH_BYTE_SIZE = 16; + public static final int LENGTH_BYTE_SIZE = 1; + public static final int OPERATION_ID_BYTE_SIZE = 32; + public static final int DELETE_FILE_OPERATION = 7; + public static final int CREATE_FILE_OPERATION = 8; + public static final int DO_SYNC_OPERATION = 9; + public static final int ERROR_REPORT_CODE = 1; + public static final int OPERATION_END_NO_REFRESH_REPORT_CODE = 3; + public static final int OPERATION_END_REPORT_CODE = 2; + public static final int REPORT_CODE_SIZE = 1; + public static final int DO_REFRESH_LENGTH = 1; + public static final int DO_REFRESH_VALUE = 1; + public static final String FILE_NAME = "fileName"; + public static final String FILE_NAME_LENGTH = FILE_NAME + "Length"; + public static final String OPERATION = "operation"; + public static final String FILE_CONTENT = "fileContent"; + public static final String FILE_CONTENT_LENGTH = FILE_CONTENT + "Length"; + public static final int DEFAULT_OPERATION = -1; + private static final String PROTOCOL_VERSION = "0.2.0"; + private byte[] handshakeMessage; + private final DigestInputStream input; + private Closeable livesyncSocket; + private OutputStream output; + + public LiveSyncWorker(LocalSocket systemSocket) throws IOException, java.security.NoSuchAlgorithmException { + this.livesyncSocket = systemSocket; + MessageDigest md = MessageDigest.getInstance("MD5"); + input = new DigestInputStream(systemSocket.getInputStream(), md); + output = systemSocket.getOutputStream(); + handshakeMessage = getHandshakeMessage(); + } + + public void run() { + try { + output.write(handshakeMessage); + output.flush(); + + } catch (IOException e) { + logger.write(String.format("Error while LiveSyncing: Client socket might be closed!", e.toString())); + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + try { + do { + int operation = getOperation(); + + if (operation == DELETE_FILE_OPERATION) { + + String fileName = getFileName(); + validateData(); + deleteRecursive(new File(DEVICE_APP_DIR, fileName)); + + } else if (operation == CREATE_FILE_OPERATION) { + + String fileName = getFileName(); + int contentLength = getFileContentLength(fileName); + validateData(); + byte[] content = getFileContent(fileName, contentLength); + validateData(); + createOrOverrideFile(fileName, content); + + } else if (operation == DO_SYNC_OPERATION) { + byte[] operationUid = readNextBytes(OPERATION_ID_BYTE_SIZE); + byte doRefresh = readNextBytes(DO_REFRESH_LENGTH)[0]; + int doRefreshInt = (int)doRefresh; + int operationReportCode; + + validateData(); + if(runtime != null && doRefreshInt == DO_REFRESH_VALUE) { + runtime.runScript(new File(NativeScriptSyncServiceSocketImpl.this.context.getFilesDir(), "internal/livesync.js"), false); + operationReportCode = OPERATION_END_REPORT_CODE; + } else { + operationReportCode = OPERATION_END_NO_REFRESH_REPORT_CODE; + } + + output.write(getReportMessageBytes(operationReportCode, operationUid)); + output.flush(); + + } else if (operation == DEFAULT_OPERATION) { + logger.write("LiveSync: input stream is empty!"); + break; + } else { + throw new IllegalArgumentException(String.format("\nLiveSync: Operation not recognised. Received operation is %s.", operation)); + } + + } while (true); + } catch (Exception e) { + String message = String.format("Error while LiveSyncing: %s", e.toString()); + this.closeWithError(message); + } catch (Throwable e) { + String message = String.format("%s(%s): Error while LiveSyncing.\nOriginal Exception: %s", Thread.currentThread().getName(), Thread.currentThread().getId(), e.toString()); + this.closeWithError(message); + } + + } + + private byte[] getErrorMessageBytes(String message) { + return this.getReportMessageBytes(ERROR_REPORT_CODE, message.getBytes()); + } + + private byte[] getReportMessageBytes(int reportType, byte[] messageBytes) { + byte[] reportBytes = new byte[]{(byte)reportType}; + byte[] combined = new byte[messageBytes.length + REPORT_CODE_SIZE]; + + System.arraycopy(reportBytes,0,combined, 0, REPORT_CODE_SIZE); + System.arraycopy(messageBytes,0,combined, REPORT_CODE_SIZE, messageBytes.length); + + return combined; + } + + private byte[] getHandshakeMessage() { + byte[] protocolVersionBytes = PROTOCOL_VERSION.getBytes(); + byte[] versionLength = new byte[]{(byte)protocolVersionBytes.length}; + byte[] packageNameBytes = context.getPackageName().getBytes(); + byte[] combined = new byte[protocolVersionBytes.length + packageNameBytes.length + versionLength.length]; + + System.arraycopy(versionLength,0,combined, 0, versionLength.length); + System.arraycopy(protocolVersionBytes,0,combined, versionLength.length, protocolVersionBytes.length); + System.arraycopy(packageNameBytes,0,combined,protocolVersionBytes.length + versionLength.length,packageNameBytes.length); + + return combined; + } + + private void validateData() throws IOException { + MessageDigest messageDigest = input.getMessageDigest(); + byte[] digest = messageDigest.digest(); + input.on(false); + byte[] inputMD5 = readNextBytes(HASH_BYTE_SIZE); + input.on(true); + + + if(!Arrays.equals(digest, inputMD5)){ + throw new IllegalStateException(String.format("\nLiveSync: Validation of data failed.\nComputed hash: %s\nOriginal hash: %s ", Arrays.toString(digest), Arrays.toString(inputMD5))); + } + } + + /* + * Tries to read operation input stream + * If the stream is empty, method returns -1 + * */ + private int getOperation() { + Integer operation; + byte[] operationBuff = null; + try { + operationBuff = readNextBytes(OPERATION_BYTE_SIZE); + if (operationBuff == null) { + return DEFAULT_OPERATION; + } + operation = Integer.parseInt(new String(operationBuff)); + + } catch (Exception e) { + if(operationBuff == null){ + operationBuff = new byte[]{}; + } + throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s. Bytes read: $s %s\nOriginal Exception: %s", OPERATION, Arrays.toString(operationBuff), e.toString())); + } + return operation; + } + + private String getFileName() { + byte[] fileNameBuffer; + int fileNameLength = -1; + + try { + fileNameLength = getLength(); + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s. \nOriginal Exception: %s", FILE_NAME_LENGTH, e.toString())); + } + + if(fileNameLength <= 0) { + throw new IllegalStateException(String.format("\nLiveSync: File name length must be positive number or zero. Provided length: %s.", fileNameLength)); + } + + try { + fileNameBuffer = readNextBytes(fileNameLength); + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: failed to parse %s.\nOriginal Exception: %s", FILE_NAME, e.toString())); + } + + if (fileNameBuffer == null) { + throw new IllegalStateException(String.format("\nLiveSync: Missing %s bytes.", FILE_NAME)); + } + + String fileName = new String(fileNameBuffer); + if (fileName.trim().length() < fileNameLength) { + logger.write(String.format("WARNING: %s parsed length is less than %s. We read less information than you specified!", FILE_NAME, FILE_NAME_LENGTH)); + } + + return fileName.trim(); + } + + private int getFileContentLength(String fileName) throws IllegalStateException { + int contentLength; + + try { + contentLength = getLength(); + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: failed to read %s for %s.\nOriginal Exception: %s", FILE_CONTENT_LENGTH, fileName, e.toString())); + } + + if(contentLength < 0){ + throw new IllegalStateException(String.format("\nLiveSync: Content length must be positive number or zero. Provided content length: %s.", contentLength)); + } + + return contentLength; + } + + private byte[] getFileContent(String fileName, int contentLength) throws IllegalStateException { + byte[] contentBuff = null; + + try { + if(contentLength > 0) { + contentBuff = readNextBytes(contentLength); + } else if(contentLength == 0){ + contentBuff = new byte[]{}; + } + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: failed to parse content of file: %s.\nOriginal Exception: %s", fileName, e.toString())); + } + + if (contentLength != 0 && contentBuff == null) { + throw new IllegalStateException(String.format("\nLiveSync: Missing %s bytes for file: %s. Did you send %s bytes?", FILE_CONTENT, fileName, contentLength)); + } + + return contentBuff; + } + + private int getLength() { + byte[] lengthBuffer; + int lengthInt; + + try { + byte lengthSize = readNextBytes(LENGTH_BYTE_SIZE)[0]; + //Cast signed byte to unsigned int + int lengthSizeInt = ((int) lengthSize) & 0xFF; + lengthBuffer = readNextBytes(lengthSizeInt); + + if (lengthBuffer == null) { + throw new IllegalStateException(String.format("\nLiveSync: Missing size length bytes.")); + } + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: Failed to read size length. \nOriginal Exception: %s", e.toString())); + } + + try { + lengthInt = Integer.valueOf(new String(lengthBuffer)); + } catch (Exception e) { + throw new IllegalStateException(String.format("\nLiveSync: Failed to parse size length. \nOriginal Exception: %s", e.toString())); + } + + return lengthInt; + } + + private void createOrOverrideFile(String fileName, byte[] content) throws IOException { + File fileToCreate = prepareFile(fileName); + try { + + fileToCreate.getParentFile().mkdirs(); + FileOutputStream fos = new FileOutputStream(fileToCreate.getCanonicalPath()); + if(runtime != null) { + runtime.lock(); + } + fos.write(content); + fos.close(); + + } catch (Exception e) { + throw new IOException(String.format("\nLiveSync: failed to write file: %s\nOriginal Exception: %s", fileName, e.toString())); + } finally { + if(runtime != null) { + runtime.unlock(); + } + } + } + + void deleteRecursive(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) + for (File child : fileOrDirectory.listFiles()) { + deleteRecursive(child); + } + + fileOrDirectory.delete(); + } + + private File prepareFile(String fileName) { + File fileToCreate = new File(DEVICE_APP_DIR, fileName); + if (fileToCreate.exists()) { + fileToCreate.delete(); + } + return fileToCreate; + } + + /* + * Reads next bites from input stream. Bytes read depend on passed parameter. + * */ + private byte[] readNextBytes(int size) throws IOException { + byte[] buffer = new byte[size]; + int bytesRead = 0; + int bufferWriteOffset = bytesRead; + try { + do { + + bytesRead = this.input.read(buffer, bufferWriteOffset, size); + if (bytesRead == -1) { + if (bufferWriteOffset == 0) { + return null; + } + break; + } + size -= bytesRead; + bufferWriteOffset += bytesRead; + } while (size > 0); + } catch (IOException e) { + String message = e.getMessage(); + if (message != null && message.equals("Try again")) { + throw new IllegalStateException("Error while LiveSyncing: Read operation timed out."); + } else { + throw e; + } + } + + return buffer; + } + + private void closeWithError(String message) { + try { + output.write(getErrorMessageBytes(message)); + output.flush(); + logger.write(message); + this.livesyncSocket.close(); + } catch (IOException e) { + if (com.tns.Runtime.isDebuggable()) { + e.printStackTrace(); + } + } + } + + @Override + protected void finalize() throws Throwable { + this.livesyncSocket.close(); + } + } +} diff --git a/platforms/android/test-app/app/src/debug/res/drawable/button.xml b/platforms/android/test-app/app/src/debug/res/drawable/button.xml new file mode 100644 index 000000000..ec005653c --- /dev/null +++ b/platforms/android/test-app/app/src/debug/res/drawable/button.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/platforms/android/test-app/app/src/debug/res/drawable/button_accented.xml b/platforms/android/test-app/app/src/debug/res/drawable/button_accented.xml new file mode 100644 index 000000000..2d5065498 --- /dev/null +++ b/platforms/android/test-app/app/src/debug/res/drawable/button_accented.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/platforms/android/test-app/app/src/debug/res/layout/error_activity.xml b/platforms/android/test-app/app/src/debug/res/layout/error_activity.xml new file mode 100644 index 000000000..9e614f6f3 --- /dev/null +++ b/platforms/android/test-app/app/src/debug/res/layout/error_activity.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + diff --git a/platforms/android/test-app/app/src/debug/res/layout/exception_tab.xml b/platforms/android/test-app/app/src/debug/res/layout/exception_tab.xml new file mode 100644 index 000000000..4d3a545f4 --- /dev/null +++ b/platforms/android/test-app/app/src/debug/res/layout/exception_tab.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + +