From 0b187661828f1506efb93b6d64e7d20c91bbcb26 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Thu, 24 Sep 2026 17:12:15 +0530 Subject: [PATCH] refactor: kotlin --- .devcontainer/Dockerfile | 4 +- .devcontainer/devcontainer.json | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/nightly-build.yml | 2 +- CONTRIBUTING.md | 12 +- dev/scripts/android.js | 41 +- dev/scripts/dev.js | 4 +- platforms/android/AGENTS.md | 4 + platforms/android/app/build.gradle | 98 ---- platforms/android/app/build.gradle.kts | 143 +++++ platforms/android/app/proguard-rules.pro | 11 +- .../foxdebug/acode/runtime/ChannelServices.kt | 5 + .../app/src/free/java/app/acode/ads/AdMob.kt | 30 +- .../free/java/app/acode/ads/ExecuteContext.kt | 8 +- .../src/free/java/app/acode/ads/ads/AdBase.kt | 6 +- .../src/free/java/app/acode/ads/ads/Banner.kt | 2 +- .../foxdebug/acode/runtime/EditionServices.kt | 9 + .../com/foxdebug/acodefree/MainActivity.java | 3 - .../android/app/src/main/AndroidManifest.xml | 287 ++++++++-- .../android/app/src/main/assets/services.json | 140 ----- .../src/main/java/com/foxdebug/acode/Acode.kt | 6 + .../java/com/foxdebug/acode/MainActivity.java | 4 +- .../acode/{rk => }/auth/Authenticator.java | 10 +- .../auth/EncryptedPreferenceManager.java | 2 +- .../foxdebug/{ => acode}/browser/Browser.java | 14 +- .../{ => acode}/browser/BrowserActivity.java | 5 +- .../{ => acode}/browser/Emulator.java | 8 +- .../foxdebug/{ => acode}/browser/Menu.java | 5 +- .../foxdebug/{ => acode}/browser/Plugin.java | 21 +- .../foxdebug}/acode/buildinfo/BuildInfo.java | 8 +- .../crashhandler/CrashActivity.java | 2 +- .../crashhandler/CrashHandler.java | 15 +- .../{rk => }/customtabs/CustomTabsPlugin.java | 6 +- .../foxdebug}/acode/device/Device.java | 13 +- .../foxdebug}/acode/file/AssetFilesystem.java | 14 +- .../acode/file/ContentFilesystem.java | 6 +- .../acode/file/DirectoryManager.java | 2 +- .../acode/file/EncodingException.java | 2 +- .../acode/file/FileExistsException.java | 2 +- .../foxdebug}/acode/file/FileUtils.java | 56 +- .../foxdebug}/acode/file/Filesystem.java | 6 +- .../file/InvalidModificationException.java | 2 +- .../foxdebug}/acode/file/LocalFilesystem.java | 6 +- .../acode/file/LocalFilesystemURL.java | 2 +- .../file/NoModificationAllowedException.java | 2 +- .../foxdebug}/acode/file/PendingRequests.java | 4 +- .../acode/file/TypeMismatchException.java | 2 +- .../com/foxdebug/{ => acode}/ftp/Ftp.java | 87 ++- .../acode/http/NativeClientAuth.java | 4 +- .../foxdebug}/acode/http/NativeHttpBase.java | 5 +- .../acode/http/NativeHttpDownload.java | 7 +- .../acode/http/NativeHttpOperation.java | 5 +- .../acode/http/NativeHttpPlugin.java | 13 +- .../acode/http/NativeHttpResponse.java | 2 +- .../acode/http/NativeHttpUpload.java | 5 +- .../http/NativeObservableCallbackContext.java | 4 +- .../acode/http/NativeServerTrust.java | 4 +- .../java/com/foxdebug/acode/runtime/App.kt | 105 ++++ .../foxdebug/acode/runtime/BaseWebActivity.kt | 242 +++++++++ .../java/com/foxdebug/acode/runtime/Bridge.kt | 112 ++++ .../com/foxdebug/acode/runtime/Callback.kt | 50 ++ .../com/foxdebug/acode/runtime/Payload.kt | 90 ++++ .../com/foxdebug/acode/runtime/Preferences.kt | 97 ++++ .../com/foxdebug/acode/runtime/ResourceApi.kt | 433 +++++++++++++++ .../acode/runtime/RuntimeBaseApplication.kt | 38 ++ .../com/foxdebug/acode/runtime/Service.kt | 82 +++ .../acode/runtime/ServiceDefinition.kt | 18 + .../com/foxdebug/acode/runtime/ServiceName.kt | 29 + .../acode/runtime/ServicePathHandler.kt} | 18 +- .../foxdebug/acode/runtime/ServicePlugin.kt | 4 + .../foxdebug/acode/runtime/ServiceRegistry.kt | 50 ++ .../foxdebug/acode/runtime/SystemBarPlugin.kt | 393 ++++++++++++++ .../runtime/webview/AcodeChromeClient.kt | 198 +++++++ .../runtime/webview/AcodeWebViewClient.kt | 118 +++++ .../acode/runtime/webview/AppWebView.kt | 154 ++++++ .../foxdebug/acode/runtime/webview/Dialogs.kt | 120 +++++ .../acode/runtime/webview/FileChooser.kt | 179 +++++++ .../runtime/webview/ImmersiveFullscreen.kt | 208 ++++++++ .../foxdebug/{ => acode}/sdcard/SDcard.java | 23 +- .../{ => acode}/sdcard/WorkspaceIndex.java | 6 +- .../acode/{rk/plugin => security}/Tee.java | 15 +- .../server/NanoHTTPDWebserver.java | 6 +- .../foxdebug/{ => acode}/server/Server.java | 9 +- .../foxdebug/acode/settings/AppPreferences.kt | 80 +++ .../com/foxdebug/{ => acode}/sftp/Sftp.java | 16 +- .../{ => acode}/sftp/SftpSecurityStore.java | 4 +- .../{ => acode}/system/RewardPassManager.java | 4 +- .../{ => acode}/system/StreamHttp.java | 6 +- .../foxdebug/{ => acode}/system/System.java | 38 +- .../com/foxdebug/{ => acode}/system/Ui.java | 2 +- .../terminal/AlpineDocumentProvider.java | 3 +- .../terminal/BackgroundExecutor.java | 10 +- .../{rk/exec => }/terminal/Executor.java | 24 +- .../exec => }/terminal/ProcessManager.java | 3 +- .../{rk/exec => }/terminal/ProcessServer.java | 2 +- .../{rk/exec => }/terminal/ProcessUtils.java | 3 +- .../{rk/exec => }/terminal/StreamHandler.java | 3 +- .../exec => }/terminal/TerminalService.java | 3 +- .../websocket/WebSocketInstance.java | 8 +- .../websocket/WebSocketPlugin.java | 8 +- .../{ => acode}/webview/WebViewActivity.java | 2 +- .../{ => acode}/webview/WebViewInstance.java | 4 +- .../{ => acode}/webview/WebViewPlugin.java | 10 +- .../java/com/verso/clipboard/Clipboard.java | 6 +- .../main/java/runner/AcodeApplication.java | 40 -- .../android/app/src/main/java/runner/App.java | 35 -- .../app/src/main/java/runner/Bridge.java | 147 ------ .../app/src/main/java/runner/Callback.java | 61 --- .../app/src/main/java/runner/Host.java | 105 ---- .../android/app/src/main/java/runner/LOG.java | 244 --------- .../src/main/java/runner/MainActivity.java | 241 --------- .../app/src/main/java/runner/Payload.java | 106 ---- .../main/java/runner/PermissionHelper.java | 14 - .../app/src/main/java/runner/Preferences.java | 104 ---- .../app/src/main/java/runner/ResourceApi.java | 496 ------------------ .../app/src/main/java/runner/Service.java | 71 --- .../src/main/java/runner/SystemBarPlugin.java | 364 ------------- .../src/main/java/runner/webView/AppView.java | 177 ------- .../java/runner/webView/ChromeClient.java | 220 -------- .../src/main/java/runner/webView/Dialogs.java | 161 ------ .../main/java/runner/webView/FileChooser.java | 178 ------- .../runner/webView/ImmersiveFullscreen.java | 221 -------- .../java/runner/webView/WebViewClient.java | 116 ---- .../foxdebug/acode/runtime/EditionServices.kt | 5 + .../android/app/src/store/AndroidManifest.xml | 5 +- .../foxdebug/acode/runtime/ChannelServices.kt | 9 + .../src/store/java/com/foxdebug/iap/Iap.java | 15 +- .../acode/runtime}/FileResourceTest.java | 18 +- .../foxdebug/acode/runtime}/PayloadTest.java | 2 +- .../foxdebug/acode/runtime}/StartupTest.java | 56 +- .../websocket/WebSocketInstanceTest.java | 19 +- platforms/android/build.gradle | 3 - platforms/android/build.gradle.kts | 4 + platforms/android/dependencies.gradle | 19 - platforms/android/gradle.properties | 7 +- .../gradle/gradle-daemon-jvm.properties | 3 +- platforms/android/gradle/libs.versions.toml | 69 ++- .../{settings.gradle => settings.gradle.kts} | 6 +- readme.md | 2 +- tests/unit/appIcons.test.js | 11 +- tests/unit/dev.test.js | 2 +- tests/unit/nativeSearchProcessing.test.js | 2 +- 142 files changed, 3668 insertions(+), 3904 deletions(-) create mode 100644 platforms/android/AGENTS.md delete mode 100644 platforms/android/app/build.gradle create mode 100644 platforms/android/app/build.gradle.kts create mode 100644 platforms/android/app/src/fdroid/java/com/foxdebug/acode/runtime/ChannelServices.kt create mode 100644 platforms/android/app/src/free/java/com/foxdebug/acode/runtime/EditionServices.kt delete mode 100644 platforms/android/app/src/free/java/com/foxdebug/acodefree/MainActivity.java delete mode 100644 platforms/android/app/src/main/assets/services.json create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/Acode.kt rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk => }/auth/Authenticator.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk => }/auth/EncryptedPreferenceManager.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/browser/Browser.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/browser/BrowserActivity.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/browser/Emulator.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/browser/Menu.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/browser/Plugin.java (58%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/buildinfo/BuildInfo.java (97%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/crashhandler/CrashActivity.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/crashhandler/CrashHandler.java (87%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk => }/customtabs/CustomTabsPlugin.java (94%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/device/Device.java (94%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/AssetFilesystem.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/ContentFilesystem.java (98%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/DirectoryManager.java (99%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/EncodingException.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/FileExistsException.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/FileUtils.java (97%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/Filesystem.java (99%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/InvalidModificationException.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/LocalFilesystem.java (99%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/LocalFilesystemURL.java (98%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/NoModificationAllowedException.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/PendingRequests.java (97%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/file/TypeMismatchException.java (96%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/ftp/Ftp.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeClientAuth.java (97%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpBase.java (99%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpDownload.java (90%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpOperation.java (90%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpPlugin.java (97%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpResponse.java (98%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeHttpUpload.java (96%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeObservableCallbackContext.java (94%) rename platforms/android/app/src/main/java/{app => com/foxdebug}/acode/http/NativeServerTrust.java (98%) create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/App.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/BaseWebActivity.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Bridge.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Callback.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Payload.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Preferences.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ResourceApi.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/RuntimeBaseApplication.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Service.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceDefinition.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceName.kt rename platforms/android/app/src/main/java/{runner/ServicePathHandler.java => com/foxdebug/acode/runtime/ServicePathHandler.kt} (70%) create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePlugin.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceRegistry.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/SystemBarPlugin.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeChromeClient.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeWebViewClient.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AppWebView.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/Dialogs.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/FileChooser.kt create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/ImmersiveFullscreen.kt rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/sdcard/SDcard.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/sdcard/WorkspaceIndex.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/plugin => security}/Tee.java (96%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/server/NanoHTTPDWebserver.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/server/Server.java (95%) create mode 100644 platforms/android/app/src/main/java/com/foxdebug/acode/settings/AppPreferences.kt rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/sftp/Sftp.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/sftp/SftpSecurityStore.java (96%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/system/RewardPassManager.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/system/StreamHttp.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/system/System.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/system/Ui.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/AlpineDocumentProvider.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/BackgroundExecutor.java (97%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/Executor.java (96%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/ProcessManager.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/ProcessServer.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/ProcessUtils.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/StreamHandler.java (90%) rename platforms/android/app/src/main/java/com/foxdebug/acode/{rk/exec => }/terminal/TerminalService.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/websocket/WebSocketInstance.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/websocket/WebSocketPlugin.java (97%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/webview/WebViewActivity.java (98%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/webview/WebViewInstance.java (99%) rename platforms/android/app/src/main/java/com/foxdebug/{ => acode}/webview/WebViewPlugin.java (98%) delete mode 100644 platforms/android/app/src/main/java/runner/AcodeApplication.java delete mode 100644 platforms/android/app/src/main/java/runner/App.java delete mode 100644 platforms/android/app/src/main/java/runner/Bridge.java delete mode 100644 platforms/android/app/src/main/java/runner/Callback.java delete mode 100644 platforms/android/app/src/main/java/runner/Host.java delete mode 100644 platforms/android/app/src/main/java/runner/LOG.java delete mode 100644 platforms/android/app/src/main/java/runner/MainActivity.java delete mode 100644 platforms/android/app/src/main/java/runner/Payload.java delete mode 100644 platforms/android/app/src/main/java/runner/PermissionHelper.java delete mode 100644 platforms/android/app/src/main/java/runner/Preferences.java delete mode 100644 platforms/android/app/src/main/java/runner/ResourceApi.java delete mode 100644 platforms/android/app/src/main/java/runner/Service.java delete mode 100644 platforms/android/app/src/main/java/runner/SystemBarPlugin.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/AppView.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/ChromeClient.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/Dialogs.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/FileChooser.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/ImmersiveFullscreen.java delete mode 100644 platforms/android/app/src/main/java/runner/webView/WebViewClient.java create mode 100644 platforms/android/app/src/paid/java/com/foxdebug/acode/runtime/EditionServices.kt create mode 100644 platforms/android/app/src/store/java/com/foxdebug/acode/runtime/ChannelServices.kt rename platforms/android/app/src/test/java/{runner => com/foxdebug/acode/runtime}/FileResourceTest.java (84%) rename platforms/android/app/src/test/java/{runner => com/foxdebug/acode/runtime}/PayloadTest.java (98%) rename platforms/android/app/src/test/java/{runner => com/foxdebug/acode/runtime}/StartupTest.java (77%) rename platforms/android/app/src/test/java/com/foxdebug/{ => acode}/websocket/WebSocketInstanceTest.java (82%) delete mode 100644 platforms/android/build.gradle create mode 100644 platforms/android/build.gradle.kts delete mode 100644 platforms/android/dependencies.gradle rename platforms/android/{settings.gradle => settings.gradle.kts} (86%) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8949094..085af67 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -7,11 +7,11 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 AS base -ARG ANDROID_PLATFORM=36 +ARG ANDROID_PLATFORM=37 ARG ANDROID_BUILD_TOOLS=36.0.0 ARG CMDLINE_TOOLS_VERSION=11076708 ARG NODE_VERSION=24 -ARG JAVA_VERSION=27 +ARG JAVA_VERSION=26 ENV JAVA_HOME=/opt/java/openjdk diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ec16d49..2b0d469 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,7 +16,7 @@ "features": { "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { - "platform": "36", + "platform": "37", "build_tools": "36.0.0" }, "ghcr.io/devcontainers/features/node:2.1": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2135769..80bd738 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: uses: actions/setup-java@v6 with: distribution: temurin - java-version: '27' + java-version: '26' - name: Run unit tests run: npm run test @@ -199,7 +199,7 @@ jobs: - uses: actions/setup-java@v6 with: distribution: temurin - java-version: '27' + java-version: '26' - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' @@ -207,7 +207,7 @@ jobs: - name: Install dependencies run: npm ci --no-audit --no-fund - name: Install Android SDK packages - run: sdkmanager 'platforms;android-36' 'build-tools;36.0.0' + run: sdkmanager 'platforms;android-37' 'build-tools;36.0.0' - name: Select package run: npm pkg set name=${{ matrix.package }} - name: Build APK diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 2299f57..b613e38 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -110,7 +110,7 @@ jobs: uses: actions/setup-java@v6 with: distribution: temurin - java-version: "27" + java-version: "26" cache: ${{ (!(inputs.is_PR && inputs.PR_NUMBER) && github.ref == 'refs/heads/main' && 'gradle') || '' }} - name: Setup Node diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5f935f..cb72fa8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,8 +82,8 @@ If you prefer not to use Docker at all: |------------|---------| | **Node.js** | 24 LTS | | **npm** | Included with Node.js | -| **Java JDK** | 27 (Eclipse Temurin) | -| **Android SDK** | API 36 | +| **Java JDK** | 26 (any vendor) | +| **Android SDK** | API 37 | | **Gradle** | 9.8.0-rc-3 (included wrapper) | ### Environment Setup @@ -102,9 +102,9 @@ export ANDROID_HOME="$HOME/Android/Sdk" export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin" ``` -Set `JAVA_HOME` to your Eclipse Temurin JDK 27 installation and add `$JAVA_HOME/bin` to `PATH`. The checked-in daemon criteria require Temurin 27, and the Gradle wrapper downloads the required Gradle version automatically. Android Gradle Plugin 9.4.1 supplies built-in Kotlin support. +Set `JAVA_HOME` to any JDK 26 installation and add `$JAVA_HOME/bin` to `PATH`. The checked-in daemon criteria require Java 26 (any vendor), and the Gradle wrapper downloads the required Gradle version automatically. Android Gradle Plugin 9.4.1 supplies built-in Kotlin support. -Java 27 support currently requires [Gradle 9.8](https://docs.gradle.org/9.8.0-rc-3/release-notes.html), whose latest release is RC3 as of 2026-09-23. Gradle 9.7.1 is the latest stable release but does not support Java 27. The wrapper pins and verifies the RC3 distribution. Java and Kotlin still emit Java 17 bytecode for Android compatibility; the build itself runs on Java 27. +The wrapper pins and verifies the Gradle 9.8.0-rc-3 distribution. Java and Kotlin still emit Java 21 bytecode for Android compatibility; the build itself runs on Java 26. ### Build Steps @@ -342,7 +342,7 @@ their file URIs; parsing them as URL text loses literal filename characters. - `src/native`: typed native APIs imported by `src/native/index.ts`; `bridge(service)` binds promise-based actions to the shared transport. - `src/platforms/android` and `src/platforms/ios`: platform transports using the shared callback and binary protocol. - `platforms/ios`: iOS app, with the template's runtime in `runner` and native services in `runner/lib`. Simulator tests and native dependencies remain alongside the app. See the [port checklist](docs/ios-port.md) for remaining work. -- `platforms/android/app/src/main/assets/services.json`: native service registration. +- `platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceRegistry.kt`: native service registration, extended by the `free`/`paid` and `store`/`fdroid` source sets. - `package.json`: app ID (`name`), version and Android version code. The native APIs are available through `Bridge.exec`, `Bridge.file`, `Bridge.http`, `Bridge.clipboard` and `Bridge.websocket`. App source uses these APIs or ordinary imports. For existing third-party plugins, `src/native/pluginCompatibility.js` exposes the legacy `cordova` namespace and module names for the public native APIs, forwarding to the same implementations. Existing direct globals and `deviceready`, pause/resume and hardware-button events remain available. Keep compatibility aliases in that file; do not use them inside Acode or add Cordova dependencies. Advertising and billing APIs retain their build-edition restrictions. @@ -386,8 +386,6 @@ The familiar APK/AAB paths remain available under `platforms/android/app/build/o `node dev/storage_manager.mjs y` or `n` toggles all-files access in the tracked Android manifest for the next build. Build scripts read package identity without rewriting it or reinstalling plugins. -See [the migration verification record](docs/native-migration.md) for behavior coverage and remaining device checks. - ## 🔧 Troubleshooting ### Missing local dependency diff --git a/dev/scripts/android.js b/dev/scripts/android.js index 5198fe0..c1dcff8 100644 --- a/dev/scripts/android.js +++ b/dev/scripts/android.js @@ -10,7 +10,7 @@ if (require.main === module) { const args = process.argv.slice(2); const options = parseOptions(args); if (args.includes("test")) { - runGradle(`test${capitalize(options.variant)}DebugUnitTest`, options); + runGradle(`test${capitalize(variantFlavor(options))}DebugUnitTest`); } else { const artifact = build(options); if (args.includes("run")) launch(options, artifact); @@ -30,7 +30,7 @@ function parseOptions(args) { return { ...getAppConfig(), mode: args.some((arg) => ["p", "prod"].includes(arg)) ? "p" : "d", - fdroid: args.includes("fdroid"), + channel: args.includes("fdroid") ? "fdroid" : "store", bundle: args.includes("bundle"), skipWeb: args.includes("--skip-web"), target: args.find((arg) => arg.startsWith("--target="))?.slice(9), @@ -50,28 +50,19 @@ function build(options) { "--mode", options.mode === "p" ? "production" : "development", ], - { ACODE_PLATFORM: "android", ACODE_FDROID: String(options.fdroid) }, + { ACODE_PLATFORM: "android", ACODE_FDROID: String(options.channel === "fdroid") }, ); } const type = options.mode === "p" ? "release" : "debug"; - const task = `${options.bundle ? "bundle" : "assemble"}${capitalize(options.variant)}${capitalize(type)}`; - runGradle(task, options); + const flavor = variantFlavor(options); + const task = `${options.bundle ? "bundle" : "assemble"}${capitalize(flavor)}${capitalize(type)}`; + runGradle(task); const outputs = path.join(root, "platforms/android/app/build/outputs"); const extension = options.bundle ? "aab" : "apk"; + const file = `app-${options.variant}-${options.channel}-${type}.${extension}`; let source = options.bundle - ? path.join( - outputs, - "bundle", - options.variant + capitalize(type), - `app-${options.variant}-${type}.${extension}`, - ) - : path.join( - outputs, - "apk", - options.variant, - type, - `app-${options.variant}-${type}.${extension}`, - ); + ? path.join(outputs, "bundle", flavor + capitalize(type), file) + : path.join(outputs, "apk", flavor, type, file); const unsigned = !options.bundle && !fs.existsSync(source); if (unsigned) source = source.replace(/\.apk$/, "-unsigned.apk"); const destination = path.join( @@ -112,15 +103,11 @@ function gradle() { process.platform === "win32" ? "gradlew.bat" : "gradlew", ); } -function runGradle(task, options) { - run(gradle(), [ - "-p", - "platforms/android", - `:app:${task}`, - `-Pfdroid=${options.fdroid}`, - `-PnodeExecutable=${process.execPath}`, - "--console=plain", - ]); +function runGradle(task) { + run(gradle(), ["-p", "platforms/android", `:app:${task}`, "--console=plain"]); +} +function variantFlavor(options) { + return options.variant + capitalize(options.channel); } function capitalize(value) { return value[0].toUpperCase() + value.slice(1); diff --git a/dev/scripts/dev.js b/dev/scripts/dev.js index 78dc284..3ef4e5a 100644 --- a/dev/scripts/dev.js +++ b/dev/scripts/dev.js @@ -252,7 +252,7 @@ async function launchApp(target, platform, emulator) { "run", "--skip-web", ]; - if (currentOptions.fdroid) args.push("fdroid"); + if (currentOptions.channel === "fdroid") args.push("fdroid"); if (currentOptions.device) args.push("--device"); if (target) args.push(`--target=${target}`); if (emulator && platform === "android") args.push("--emulator"); @@ -267,7 +267,7 @@ function startRspackWatch(host, port, proto, onCompiled) { const env = buildSpawnEnv({ DEV_MODE: "true", ACODE_PLATFORM: currentOptions.platform, - ACODE_FDROID: String(!!currentOptions.fdroid), + ACODE_FDROID: String(currentOptions.channel === "fdroid"), DEV_HOST: host, DEV_PORT: String(port), DEV_PROTO: proto, diff --git a/platforms/android/AGENTS.md b/platforms/android/AGENTS.md new file mode 100644 index 0000000..37b5d3d --- /dev/null +++ b/platforms/android/AGENTS.md @@ -0,0 +1,4 @@ +- Always use kotlin for writing new code +- Avoid java +- Always add keep rules in proguard file for add @Keep annotation if a file must be preserved +- always write idiomatic android/kotlin code never take shortcuts \ No newline at end of file diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle deleted file mode 100644 index 5dfbd2b..0000000 --- a/platforms/android/app/build.gradle +++ /dev/null @@ -1,98 +0,0 @@ -import groovy.json.JsonSlurper - -plugins { - id 'com.android.application' -} - -def acode = new JsonSlurper().parse(file('../../../package.json')) -def selectedEdition = ['com.foxdebug.acode': 'paid', 'com.foxdebug.acodefree': 'free'][acode.name] -if (!selectedEdition) throw new GradleException('Set package.json name to com.foxdebug.acode (paid) or com.foxdebug.acodefree (free).') -def fdroid = providers.gradleProperty('fdroid').orElse('false').get().toBoolean() - -android { - namespace = 'com.foxdebug.acode' - compileSdk = 36 - buildToolsVersion = '36.0.0' - defaultConfig { - minSdk = 26 - targetSdk = fdroid ? 28 : 36 - versionCode = acode.versionCode - versionName = acode.version - buildConfigField 'boolean', 'FDROID', fdroid.toString() - } - flavorDimensions 'edition' - productFlavors { - paid { dimension = 'edition'; applicationId = 'com.foxdebug.acode' } - free { dimension = 'edition'; applicationId = 'com.foxdebug.acodefree' } - } - buildFeatures { buildConfig = true; resValues = true } - sourceSets { - main { - if (!fdroid) { - java.srcDir 'src/store/java' - assets.srcDir 'src/store/assets' - jniLibs.srcDir 'src/store/jniLibs' - } - } - } - signingConfigs { - release {} - def configFile = file('../../../build.json') - if (configFile.exists()) { - def configurations = new JsonSlurper().parse(configFile).android - ['debug', 'release'].each { type -> - def config = configurations[type] - if (config?.keystore) { - getByName(type) { - storeFile = new File(config.keystore).isAbsolute() ? file(config.keystore) : file('../../../' + config.keystore) - storePassword = config.storePassword - keyAlias = config.alias - keyPassword = config.password - if (config.keystoreType) storeType = config.keystoreType - } - } - } - } - } - buildTypes { - debug { debuggable = true } - release { - minifyEnabled = true - shrinkResources = true - if (signingConfigs.release.storeFile) signingConfig = signingConfigs.release - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - packaging { - jniLibs { useLegacyPackaging = true } - resources { pickFirsts += 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' } - } - testOptions { unitTests.includeAndroidResources = true } -} - -configurations.configureEach { - exclude module: 'commons-logging' - exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' - exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on' - exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on' - exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on' -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.7.1' - implementation 'androidx.webkit:webkit:1.14.0' - implementation 'androidx.core:core-splashscreen:1.0.1' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.17' -} -apply from: '../dependencies.gradle' - -androidComponents { - beforeVariants(selector().all()) { variant -> - variant.enable = variant.productFlavors[0].second == selectedEdition - } -} diff --git a/platforms/android/app/build.gradle.kts b/platforms/android/app/build.gradle.kts new file mode 100644 index 0000000..0128087 --- /dev/null +++ b/platforms/android/app/build.gradle.kts @@ -0,0 +1,143 @@ +import groovy.json.JsonSlurper +import java.io.File + +plugins { + alias(libs.plugins.android.application) +} + +val acode = file("../../../package.json").parseJsonObject() +val selectedEdition = mapOf( + "com.foxdebug.acode" to "paid", + "com.foxdebug.acodefree" to "free" +)[acode["name"] as String] + ?: throw GradleException("Set package.json name to com.foxdebug.acode (paid) or com.foxdebug.acodefree (free).") + +android { + //this is not package name do not change + namespace = "com.foxdebug.acode" + compileSdk = 37 + buildToolsVersion = "36.0.0" + defaultConfig { + minSdk = 26 + targetSdk = 37 + versionCode = (acode["versionCode"] as Number).toInt() + versionName = acode["version"] as String + } + flavorDimensions += listOf("edition", "channel") + productFlavors { + create("paid") { + dimension = "edition" + applicationId = "com.foxdebug.acode" + } + create("free") { + dimension = "edition" + applicationId = "com.foxdebug.acodefree" + } + create("store") { + dimension = "channel" + buildConfigField("boolean", "FDROID", "false") + } + create("fdroid") { + dimension = "channel" + targetSdk = 28 + buildConfigField("boolean", "FDROID", "true") + } + } + buildFeatures { + buildConfig = true + resValues = true + } + signingConfigs { + create("release") + val configFile = file("../../../build.json") + if (configFile.exists()) { + val signingConfigurations = configFile.parseJsonObject().jsonObject("android") + listOf("debug", "release").forEach { type -> + val config = signingConfigurations?.jsonObject(type) ?: return@forEach + val keystore = config["keystore"] as? String ?: return@forEach + getByName(type) { + storeFile = if (File(keystore).isAbsolute) file(keystore) else file("../../../$keystore") + storePassword = config["storePassword"] as? String + keyAlias = config["alias"] as? String + keyPassword = config["password"] as? String + (config["keystoreType"] as? String)?.let { storeType = it } + } + } + } + } + buildTypes { + getByName("debug") { + isDebuggable = true + } + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + if (signingConfigs.getByName("release").storeFile != null) { + signingConfig = signingConfigs.getByName("release") + } + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + packaging { + jniLibs { + useLegacyPackaging = true + } + resources { + pickFirsts += "META-INF/versions/9/OSGI-INF/MANIFEST.MF" + } + } + testOptions { + unitTests.isIncludeAndroidResources = true + } +} + +configurations.configureEach { + exclude(module = "commons-logging") + exclude(group = "org.bouncycastle", module = "bcprov-jdk15on") + exclude(group = "org.bouncycastle", module = "bcpkix-jdk15on") + exclude(group = "org.bouncycastle", module = "bcpkix-jdk18on") + exclude(group = "org.bouncycastle", module = "bcprov-jdk18on") +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.browser) + implementation(libs.androidx.core) + implementation(libs.androidx.core.google.shortcuts) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.documentfile) + implementation(libs.androidx.security.crypto) + implementation(libs.androidx.webkit) + implementation(libs.bouncycastle.bcpkix) + implementation(libs.bouncycastle.bcprov) + implementation(libs.commons.codec) + implementation(libs.commons.io) + implementation(libs.commons.net) + implementation(libs.java.websocket) + implementation(libs.maverick.synergy.client) + implementation(libs.nanohttpd) + implementation(libs.okhttp) + + //check product flavours + "freeImplementation"(libs.play.services.ads) + "freeImplementation"(libs.user.messaging.platform) + "storeImplementation"(libs.billing.client) + testImplementation(libs.junit) + testImplementation(libs.robolectric) +} + +androidComponents { + beforeVariants(selector().all()) { variant -> + variant.enable = variant.productFlavors.first { it.first == "edition" }.second == selectedEdition + } +} + +@Suppress("UNCHECKED_CAST") +fun File.parseJsonObject(): Map = JsonSlurper().parse(this) as Map + +@Suppress("UNCHECKED_CAST") +fun Map.jsonObject(key: String): Map? = this[key] as? Map diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro index 28aeffc..1f0218b 100644 --- a/platforms/android/app/proguard-rules.pro +++ b/platforms/android/app/proguard-rules.pro @@ -1,13 +1,12 @@ # Keep Acode classes --keep class runner.** { *; } +-keep class com.foxdebug.acode.runtime.** { *; } -# Acode discovers plugins by class name from services.json and instantiates them -# reflectively, and plugins can dispatch actions by method name through -# reflection too (e.g. Ftp.execute() uses +# Services are registered by type in ServiceRegistry, but a few of them dispatch +# actions by method name through reflection (e.g. Ftp.execute() uses # getClass().getDeclaredMethod(action, JSONArray, Callback)). # Without keeping the members, R8 strips methods such as connect()/listDirectory() -# and those plugin calls fail at runtime with NoSuchMethodException. --keep public class * extends runner.Service { *; } +# and those calls fail at runtime with NoSuchMethodException. +-keep public class * extends com.foxdebug.acode.runtime.Service { *; } # WebView JS bridge methods are invoked by name from JavaScript. -keepclassmembers class * { diff --git a/platforms/android/app/src/fdroid/java/com/foxdebug/acode/runtime/ChannelServices.kt b/platforms/android/app/src/fdroid/java/com/foxdebug/acode/runtime/ChannelServices.kt new file mode 100644 index 0000000..4791604 --- /dev/null +++ b/platforms/android/app/src/fdroid/java/com/foxdebug/acode/runtime/ChannelServices.kt @@ -0,0 +1,5 @@ +package com.foxdebug.acode.runtime + +internal object ChannelServices : ServiceModule { + override val services: List = emptyList() +} diff --git a/platforms/android/app/src/free/java/app/acode/ads/AdMob.kt b/platforms/android/app/src/free/java/app/acode/ads/AdMob.kt index e2eaff2..8ff5b62 100644 --- a/platforms/android/app/src/free/java/app/acode/ads/AdMob.kt +++ b/platforms/android/app/src/free/java/app/acode/ads/AdMob.kt @@ -18,9 +18,9 @@ import android.util.Log import android.view.ViewGroup import android.webkit.WebView import com.google.android.gms.ads.MobileAds -import runner.Callback -import runner.Service -import runner.Payload +import com.foxdebug.acode.runtime.Callback +import com.foxdebug.acode.runtime.Service +import com.foxdebug.acode.runtime.Payload import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -85,7 +85,7 @@ class AdMob : Service() { readyCallbackContext = ctx.callbackContext emit( Events.READY, - mapOf("isRunningInTestLab" to isRunningInTestLab(host.activity)) + mapOf("isRunningInTestLab" to isRunningInTestLab(host!!.activity)) ) return true } @@ -143,14 +143,14 @@ class AdMob : Service() { private fun executeAdDestroy(ctx: ExecuteContext) { val id = ctx.optId() ?: return ctx.reject("id is required") - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { ads[id]?.onDestroy() ctx.resolve() } } private fun executeAdIsLoaded(ctx: ExecuteContext) { - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { ctx.optAdOrReject()?.let { ad -> ctx.resolve(ad.isLoaded) } @@ -158,7 +158,7 @@ class AdMob : Service() { } private fun executeAdLoad(ctx: ExecuteContext) { - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { ctx.optAdOrReject()?.let { ad -> ad.load(ctx) } @@ -166,7 +166,7 @@ class AdMob : Service() { } private fun executeAdShow(ctx: ExecuteContext) { - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { ctx.optAdOrReject()?.let { ad -> if (shouldDispatchAdShow(ad.isLoaded, ad.canShowWhileLoading)) { ad.show(ctx) @@ -178,24 +178,24 @@ class AdMob : Service() { } private fun executeAdHide(ctx: ExecuteContext) { - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { ctx.optAdOrReject()?.hide(ctx) } } private fun executeWebviewGoto(ctx: ExecuteContext) { - host.activity.runOnUiThread { - val webView = webView.view as WebView + host!!.activity.runOnUiThread { + val webView = webView!!.view as WebView webView.loadUrl(ctx.args.getString(0)) ctx.resolve() } } - val activity: Activity get() = host.activity + val activity: Activity get() = host!!.activity val contentView: ViewGroup? get() = activity.findViewById(android.R.id.content) - ?: getParentView(webView.view) + ?: getParentView(webView!!.view) fun emit(eventName: String, data: Map) { @@ -205,7 +205,7 @@ class AdMob : Service() { readyCallbackContext?.sendPayload(result) ?: eventQueue.add(result) } - override fun onConfigurationChanged(newConfig: Configuration) { + override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) ads.forEach { (_, ad) -> ad.onConfigurationChanged(newConfig) @@ -242,7 +242,7 @@ class AdMob : Service() { val previousAds = synchronized(ads) { ads.values.toList().also { ads.clear() } } - host.activity.runOnUiThread { + host!!.activity.runOnUiThread { for (ad in previousAds) { ad.onDestroy() } diff --git a/platforms/android/app/src/free/java/app/acode/ads/ExecuteContext.kt b/platforms/android/app/src/free/java/app/acode/ads/ExecuteContext.kt index 5fa17c6..6f91783 100644 --- a/platforms/android/app/src/free/java/app/acode/ads/ExecuteContext.kt +++ b/platforms/android/app/src/free/java/app/acode/ads/ExecuteContext.kt @@ -2,8 +2,8 @@ package app.acode.ads import app.acode.ads.ads.AdBase import android.app.Activity -import runner.Callback -import runner.Payload +import com.foxdebug.acode.runtime.Callback +import com.foxdebug.acode.runtime.Payload import org.json.JSONArray import org.json.JSONObject import java.util.Collections.synchronizedMap @@ -18,7 +18,7 @@ data class ExecuteContext( ) { val opts: JSONObject by lazy { args.optJSONObject(0) } - val activity: Activity get() = plugin.host.activity + val activity: Activity get() = plugin.host!!.activity fun optBoolean(name: String): Boolean? { return if (opts.has(name)) opts.optBoolean(name) else null @@ -45,7 +45,7 @@ data class ExecuteContext( callbackContext.error(msg) } - fun sendResult(result: Payload?) { + fun sendResult(result: Payload) { callbackContext.sendPayload(result) } diff --git a/platforms/android/app/src/free/java/app/acode/ads/ads/AdBase.kt b/platforms/android/app/src/free/java/app/acode/ads/ads/AdBase.kt index e04f75f..9692432 100644 --- a/platforms/android/app/src/free/java/app/acode/ads/ads/AdBase.kt +++ b/platforms/android/app/src/free/java/app/acode/ads/ads/AdBase.kt @@ -13,7 +13,7 @@ import com.google.android.gms.ads.AdValue import com.google.android.gms.ads.OnPaidEventListener import com.google.android.gms.ads.ResponseInfo import com.google.android.gms.ads.rewarded.RewardItem -import runner.webView.AppView +import com.foxdebug.acode.runtime.webview.AppWebView import org.json.JSONObject fun getParentView(view: View?): ViewGroup? { @@ -35,7 +35,7 @@ abstract class AdBase(ctx: ExecuteContext) { protected val plugin = ctx.plugin - private val BridgeWebView: AppView get() = plugin.webView + private val BridgeWebView: AppWebView get() = plugin.webView!! val webView: View get() = BridgeWebView.view val webViewParent: ViewGroup get() = (webView.parent as? ViewGroup) ?: throw IllegalStateException("webView has no parent") @@ -49,7 +49,7 @@ abstract class AdBase(ctx: ExecuteContext) { } } - open fun onConfigurationChanged(newConfig: Configuration) {} + open fun onConfigurationChanged(newConfig: Configuration?) {} open fun onPause(multitasking: Boolean) {} open fun onResume(multitasking: Boolean) {} open fun onDestroy() { diff --git a/platforms/android/app/src/free/java/app/acode/ads/ads/Banner.kt b/platforms/android/app/src/free/java/app/acode/ads/ads/Banner.kt index 53db24c..88469a1 100644 --- a/platforms/android/app/src/free/java/app/acode/ads/ads/Banner.kt +++ b/platforms/android/app/src/free/java/app/acode/ads/ads/Banner.kt @@ -264,7 +264,7 @@ class Banner(ctx: ExecuteContext) : AdBase(ctx) { ctx.resolve() } - override fun onConfigurationChanged(newConfig: Configuration) { + override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) plugin.activity.runOnUiThread { webView.post { diff --git a/platforms/android/app/src/free/java/com/foxdebug/acode/runtime/EditionServices.kt b/platforms/android/app/src/free/java/com/foxdebug/acode/runtime/EditionServices.kt new file mode 100644 index 0000000..13762ae --- /dev/null +++ b/platforms/android/app/src/free/java/com/foxdebug/acode/runtime/EditionServices.kt @@ -0,0 +1,9 @@ +package com.foxdebug.acode.runtime + +import app.acode.ads.AdMob + +internal object EditionServices : ServiceModule { + override val services: List = listOf( + ServiceDefinition(ServiceName.AD_MOB) { AdMob() }, + ) +} diff --git a/platforms/android/app/src/free/java/com/foxdebug/acodefree/MainActivity.java b/platforms/android/app/src/free/java/com/foxdebug/acodefree/MainActivity.java deleted file mode 100644 index 20f50d3..0000000 --- a/platforms/android/app/src/free/java/com/foxdebug/acodefree/MainActivity.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.foxdebug.acodefree; - -public final class MainActivity extends runner.MainActivity {} diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index 29d53c1..97e42e1 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -1,21 +1,70 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + @@ -28,138 +77,282 @@ - + + + + + + + + + + + + + - - - - - - + + + + + - + - - - - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - + + + \ No newline at end of file diff --git a/platforms/android/app/src/main/assets/services.json b/platforms/android/app/src/main/assets/services.json deleted file mode 100644 index d280c7d..0000000 --- a/platforms/android/app/src/main/assets/services.json +++ /dev/null @@ -1,140 +0,0 @@ -[ - { - "name": "AdMob", - "class": "app.acode.ads.AdMob", - "onload": false, - "variant": "free" - }, - { - "name": "Authenticator", - "class": "com.foxdebug.acode.rk.auth.Authenticator", - "onload": true, - "variant": "main" - }, - { - "name": "Browser", - "class": "com.foxdebug.browser.Plugin", - "onload": false, - "variant": "main" - }, - { - "name": "Clipboard", - "class": "com.verso.clipboard.Clipboard", - "onload": false, - "variant": "main" - }, - { - "name": "NativeHttpPlugin", - "class": "app.acode.http.NativeHttpPlugin", - "onload": false, - "variant": "main" - }, - { - "name": "BuildInfo", - "class": "app.acode.buildinfo.BuildInfo", - "onload": false, - "variant": "main" - }, - { - "name": "CrashHandler", - "class": "com.foxdebug.crashhandler.CrashHandler", - "onload": true, - "variant": "main" - }, - { - "name": "CustomTabs", - "class": "com.foxdebug.acode.rk.customtabs.CustomTabsPlugin", - "onload": false, - "variant": "main" - }, - { - "name": "Device", - "class": "app.acode.device.Device", - "onload": false, - "variant": "main" - }, - { - "name": "File", - "class": "app.acode.file.FileUtils", - "onload": true, - "variant": "main" - }, - { - "name": "Ftp", - "class": "com.foxdebug.ftp.Ftp", - "onload": false, - "variant": "main" - }, - { - "name": "Iap", - "class": "com.foxdebug.iap.Iap", - "onload": false, - "variant": "store" - }, - { - "name": "Tee", - "class": "com.foxdebug.acode.rk.plugin.Tee", - "onload": false, - "variant": "main" - }, - { - "name": "SDcard", - "class": "com.foxdebug.sdcard.SDcard", - "onload": false, - "variant": "main" - }, - { - "name": "Server", - "class": "com.foxdebug.server.Server", - "onload": false, - "variant": "main" - }, - { - "name": "Sftp", - "class": "com.foxdebug.sftp.Sftp", - "onload": false, - "variant": "main" - }, - { - "name": "System", - "class": "com.foxdebug.system.System", - "onload": false, - "variant": "main" - }, - { - "name": "Executor", - "class": "com.foxdebug.acode.rk.exec.terminal.Executor", - "onload": false, - "variant": "main" - }, - { - "name": "BackgroundExecutor", - "class": "com.foxdebug.acode.rk.exec.terminal.BackgroundExecutor", - "onload": false, - "variant": "main" - }, - { - "name": "WebSocketPlugin", - "class": "com.foxdebug.websocket.WebSocketPlugin", - "onload": false, - "variant": "main" - }, - { - "name": "AcodeWebView", - "class": "com.foxdebug.webview.WebViewPlugin", - "onload": false, - "variant": "main" - }, - { - "name": "App", - "class": "runner.App", - "onload": false, - "variant": "main" - }, - { - "name": "SystemBarPlugin", - "class": "runner.SystemBarPlugin", - "onload": true, - "variant": "main" - } -] diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/Acode.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/Acode.kt new file mode 100644 index 0000000..26d0eb2 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/Acode.kt @@ -0,0 +1,6 @@ +package com.foxdebug.acode + +import com.foxdebug.acode.runtime.RuntimeBaseApplication + +class Acode : RuntimeBaseApplication() { +} \ No newline at end of file diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java b/platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java index 3402863..8e7db49 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java @@ -1,3 +1,5 @@ package com.foxdebug.acode; -public final class MainActivity extends runner.MainActivity {} +import com.foxdebug.acode.runtime.BaseWebActivity; + +public final class MainActivity extends BaseWebActivity {} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/Authenticator.java b/platforms/android/app/src/main/java/com/foxdebug/acode/auth/Authenticator.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/Authenticator.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/auth/Authenticator.java index e995202..1d79697 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/Authenticator.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/auth/Authenticator.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.auth; +package com.foxdebug.acode.auth; import android.content.Intent; import android.net.Uri; @@ -6,7 +6,7 @@ import android.webkit.CookieManager; import android.webkit.WebView; import androidx.browser.customtabs.CustomTabsIntent; -import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; + import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; @@ -16,8 +16,8 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -45,7 +45,7 @@ public class Authenticator extends Service { private volatile Callback loginCallback; @Override - protected void serviceInitialize() { + public void serviceInitialize() { Log.d(TAG, "Initializing Authenticator Plugin..."); this.prefManager = new EncryptedPreferenceManager(this.host.getContext(), PREFS_FILENAME); diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/EncryptedPreferenceManager.java b/platforms/android/app/src/main/java/com/foxdebug/acode/auth/EncryptedPreferenceManager.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/EncryptedPreferenceManager.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/auth/EncryptedPreferenceManager.java index 51a32a1..6a8ee27 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/auth/EncryptedPreferenceManager.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/auth/EncryptedPreferenceManager.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.auth; +package com.foxdebug.acode.auth; import android.content.Context; import android.content.SharedPreferences; diff --git a/platforms/android/app/src/main/java/com/foxdebug/browser/Browser.java b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Browser.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/browser/Browser.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/browser/Browser.java index f125971..48eeec7 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/browser/Browser.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Browser.java @@ -1,16 +1,13 @@ -package com.foxdebug.browser; +package com.foxdebug.acode.browser; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.net.Uri; -import android.os.Build; import android.text.InputType; import android.text.TextUtils; -import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -18,7 +15,6 @@ import android.view.ViewTreeObserver; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.webkit.ConsoleMessage; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; @@ -31,21 +27,17 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import com.foxdebug.browser.Emulator; -import com.foxdebug.browser.Menu; -import com.foxdebug.system.Ui; + +import com.foxdebug.acode.system.Ui; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import android.app.AlertDialog; import android.app.DownloadManager; -import android.content.Context; -import android.net.Uri; import android.os.Environment; import android.webkit.DownloadListener; import android.webkit.URLUtil; -import android.webkit.WebView; import android.widget.Toast; import android.os.Handler; import android.os.Looper; diff --git a/platforms/android/app/src/main/java/com/foxdebug/browser/BrowserActivity.java b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/BrowserActivity.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/browser/BrowserActivity.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/browser/BrowserActivity.java index ddca862..e624f86 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/browser/BrowserActivity.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/BrowserActivity.java @@ -1,8 +1,7 @@ -package com.foxdebug.browser; +package com.foxdebug.acode.browser; import android.app.Activity; import android.content.Intent; -import android.graphics.Color; import android.graphics.Insets; import android.os.Build; import android.os.Bundle; @@ -13,7 +12,7 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import android.webkit.WebChromeClient; -import com.foxdebug.system.Ui; +import com.foxdebug.acode.system.Ui; import org.json.JSONObject; public class BrowserActivity extends Activity { diff --git a/platforms/android/app/src/main/java/com/foxdebug/browser/Emulator.java b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Emulator.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/browser/Emulator.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/browser/Emulator.java index adb65f2..86cff6f 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/browser/Emulator.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Emulator.java @@ -1,4 +1,4 @@ -package com.foxdebug.browser; +package com.foxdebug.acode.browser; import android.content.Context; import android.graphics.Typeface; @@ -10,10 +10,8 @@ import android.widget.ScrollView; import android.widget.SeekBar; import android.widget.TextView; -import com.foxdebug.system.Ui; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import com.foxdebug.acode.system.Ui; + import java.util.HashMap; public class Emulator extends LinearLayout { diff --git a/platforms/android/app/src/main/java/com/foxdebug/browser/Menu.java b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Menu.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/browser/Menu.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/browser/Menu.java index 8b5bbb5..41ea196 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/browser/Menu.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Menu.java @@ -1,10 +1,9 @@ -package com.foxdebug.browser; +package com.foxdebug.acode.browser; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.GradientDrawable; -import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -16,7 +15,7 @@ import android.widget.ScrollView; import android.widget.TextView; import com.foxdebug.acode.R; -import com.foxdebug.system.Ui; +import com.foxdebug.acode.system.Ui; public class Menu extends PopupWindow { diff --git a/platforms/android/app/src/main/java/com/foxdebug/browser/Plugin.java b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Plugin.java similarity index 58% rename from platforms/android/app/src/main/java/com/foxdebug/browser/Plugin.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/browser/Plugin.java index 07e4911..7c94149 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/browser/Plugin.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/browser/Plugin.java @@ -1,14 +1,23 @@ -package com.foxdebug.browser; +package com.foxdebug.acode.browser; import android.content.Intent; -import com.foxdebug.browser.BrowserActivity; -import runner.Callback; -import runner.Service; + +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; + +import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class Plugin extends Service { + BridgeContext bridgeContext; + + @Override + public void initialize(@NotNull BridgeContext bridgeContext) { + this.bridgeContext = bridgeContext; + } @Override public boolean execute( @@ -21,12 +30,12 @@ public boolean execute( JSONObject theme = args.getJSONObject(1); boolean onlyConsole = args.optBoolean(2, false); String themeString = theme.toString(); - Intent intent = new Intent(host.getActivity(), BrowserActivity.class); + Intent intent = new Intent(bridgeContext.getWebActivity(), BrowserActivity.class); intent.putExtra("url", url); intent.putExtra("theme", themeString); intent.putExtra("onlyConsole", onlyConsole); - host.getActivity().startActivity(intent); + bridgeContext.getWebActivity().startActivity(intent); callbackContext.success("Opened browser"); return true; } diff --git a/platforms/android/app/src/main/java/app/acode/buildinfo/BuildInfo.java b/platforms/android/app/src/main/java/com/foxdebug/acode/buildinfo/BuildInfo.java similarity index 97% rename from platforms/android/app/src/main/java/app/acode/buildinfo/BuildInfo.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/buildinfo/BuildInfo.java index 340df52..5dc2ac3 100644 --- a/platforms/android/app/src/main/java/app/acode/buildinfo/BuildInfo.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/buildinfo/BuildInfo.java @@ -22,15 +22,15 @@ of this software and associated documentation files (the "Software"), to deal SOFTWARE. */ -package app.acode.buildinfo; +package com.foxdebug.acode.buildinfo; import android.app.Activity; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log; -import runner.Callback; -import runner.Service; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -152,7 +152,7 @@ private void init(String buildConfigClassName, Callback callbackContext) { //mBuildInfoCache.put("buildDate" , convertLongToDateTimeString(getClassFieldLong(c, "_BUILDINFO_TIMESTAMP", 0L))); mBuildInfoCache.put("installDate" , convertLongToDateTimeString(firstInstallTime)); mBuildInfoCache.put("buildType" , getClassFieldString(c, "BUILD_TYPE", "")); - mBuildInfoCache.put("flavor" , getClassFieldString(c, "FLAVOR", "")); + mBuildInfoCache.put("flavor" , getClassFieldString(c, "FLAVOR_edition", getClassFieldString(c, "FLAVOR", ""))); if (debug) { Log.d(TAG, "packageName : \"" + mBuildInfoCache.getString("packageName") + "\""); diff --git a/platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashActivity.java b/platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashActivity.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashActivity.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashActivity.java index e9cd989..be00bf9 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashActivity.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashActivity.java @@ -1,4 +1,4 @@ -package com.foxdebug.crashhandler; +package com.foxdebug.acode.crashhandler; import android.app.Activity; import android.content.ClipData; diff --git a/platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashHandler.java b/platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashHandler.java similarity index 87% rename from platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashHandler.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashHandler.java index 65d7755..8bee6e0 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/crashhandler/CrashHandler.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/crashhandler/CrashHandler.java @@ -1,21 +1,21 @@ -package com.foxdebug.crashhandler; +package com.foxdebug.acode.crashhandler; import android.content.Context; import android.content.Intent; import android.util.Log; import java.io.PrintWriter; import java.io.StringWriter; -import runner.Host; -import runner.Service; -import runner.webView.AppView; + +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Service; public class CrashHandler extends Service { private static final String TAG = "CrashHandler"; @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); Log.d(TAG, "Initializing CrashHandler..."); Thread.setDefaultUncaughtExceptionHandler( @@ -30,8 +30,7 @@ public void uncaughtException(Thread thread, Throwable ex) { ex.printStackTrace(pw); String stackTrace = sw.toString(); - Context context = host - .getActivity() + Context context = bridgeContext.getWebActivity() .getApplicationContext(); Intent intent = new Intent( context, diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/customtabs/CustomTabsPlugin.java b/platforms/android/app/src/main/java/com/foxdebug/acode/customtabs/CustomTabsPlugin.java similarity index 94% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/customtabs/CustomTabsPlugin.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/customtabs/CustomTabsPlugin.java index ae93a78..33d47ad 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/customtabs/CustomTabsPlugin.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/customtabs/CustomTabsPlugin.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.customtabs; +package com.foxdebug.acode.customtabs; import android.content.ActivityNotFoundException; import android.content.Intent; @@ -7,8 +7,8 @@ import androidx.browser.customtabs.CustomTabsIntent; -import runner.Callback; -import runner.Service; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/app/acode/device/Device.java b/platforms/android/app/src/main/java/com/foxdebug/acode/device/Device.java similarity index 94% rename from platforms/android/app/src/main/java/app/acode/device/Device.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/device/Device.java index 1d88d7a..1d08d5a 100644 --- a/platforms/android/app/src/main/java/app/acode/device/Device.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/device/Device.java @@ -16,14 +16,13 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.device; +package com.foxdebug.acode.device; import java.util.TimeZone; -import runner.webView.AppView; -import runner.Callback; -import runner.Service; -import runner.Host; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -53,8 +52,8 @@ public Device() { * @param Bridge The context of the main Activity. * @param webView The AppView Native is running in. */ - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); Device.uuid = getUuid(); } diff --git a/platforms/android/app/src/main/java/app/acode/file/AssetFilesystem.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/AssetFilesystem.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/AssetFilesystem.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/AssetFilesystem.java index d2f269e..55f70e7 100644 --- a/platforms/android/app/src/main/java/app/acode/file/AssetFilesystem.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/AssetFilesystem.java @@ -16,14 +16,14 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.content.res.AssetManager; import android.net.Uri; +import android.util.Log; -import runner.Preferences; -import runner.ResourceApi; -import runner.LOG; +import com.foxdebug.acode.runtime.Preferences; +import com.foxdebug.acode.runtime.ResourceApi; import org.json.JSONException; import org.json.JSONObject; @@ -65,12 +65,12 @@ private void lazyInitCaches() { try { ois.close(); } catch (IOException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); + Log.d(LOG_TAG, e.getLocalizedMessage()); } } } if (listCache == null) { - LOG.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow."); + Log.w("AssetFilesystem", "Asset manifest not found. Recursive copies and directory listing will be slow."); listCache = new HashMap(); } } @@ -127,7 +127,7 @@ private long getAssetSize(String assetPath) throws FileNotFoundException { try { offr.inputStream.close(); } catch (IOException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); + Log.d(LOG_TAG, e.getLocalizedMessage()); } } } diff --git a/platforms/android/app/src/main/java/app/acode/file/ContentFilesystem.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/ContentFilesystem.java similarity index 98% rename from platforms/android/app/src/main/java/app/acode/file/ContentFilesystem.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/ContentFilesystem.java index 58e5353..35d4cd1 100644 --- a/platforms/android/app/src/main/java/app/acode/file/ContentFilesystem.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/ContentFilesystem.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.content.ContentResolver; import android.content.Context; @@ -29,8 +29,8 @@ Licensed to the Apache Software Foundation (ASF) under one import java.io.FileNotFoundException; import java.io.IOException; -import runner.Preferences; -import runner.ResourceApi; +import com.foxdebug.acode.runtime.Preferences; +import com.foxdebug.acode.runtime.ResourceApi; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/app/acode/file/DirectoryManager.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/DirectoryManager.java similarity index 99% rename from platforms/android/app/src/main/java/app/acode/file/DirectoryManager.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/DirectoryManager.java index 464f5f7..fa6c211 100644 --- a/platforms/android/app/src/main/java/app/acode/file/DirectoryManager.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/DirectoryManager.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.os.Environment; import android.os.StatFs; diff --git a/platforms/android/app/src/main/java/app/acode/file/EncodingException.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/EncodingException.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/EncodingException.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/EncodingException.java index 979c8cb..1e2c20c 100644 --- a/platforms/android/app/src/main/java/app/acode/file/EncodingException.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/EncodingException.java @@ -17,7 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; @SuppressWarnings("serial") public class EncodingException extends Exception { diff --git a/platforms/android/app/src/main/java/app/acode/file/FileExistsException.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/FileExistsException.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/FileExistsException.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/FileExistsException.java index 62b01a9..20a8711 100644 --- a/platforms/android/app/src/main/java/app/acode/file/FileExistsException.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/FileExistsException.java @@ -17,7 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; @SuppressWarnings("serial") public class FileExistsException extends Exception { diff --git a/platforms/android/app/src/main/java/app/acode/file/FileUtils.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/FileUtils.java similarity index 97% rename from platforms/android/app/src/main/java/app/acode/file/FileUtils.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/FileUtils.java index 0af8a76..f083c76 100644 --- a/platforms/android/app/src/main/java/app/acode/file/FileUtils.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/FileUtils.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.Manifest; import android.app.Activity; @@ -32,22 +32,20 @@ Licensed to the Apache Software Foundation (ASF) under one import androidx.webkit.WebViewAssetLoader; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.ServicePathHandler; -import runner.ResourceApi; -import runner.webView.AppView; -import runner.LOG; -import runner.PermissionHelper; -import runner.Payload; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.ServicePathHandler; +import com.foxdebug.acode.runtime.ResourceApi; +import com.foxdebug.acode.runtime.Payload; + +import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -136,10 +134,10 @@ protected void registerExtraFileSystems(String[] filesystems, HashMap getAvailableFileSystems(Activity activity) { availableFileSystems.put("sdcard", Environment.getExternalStorageDirectory().getAbsolutePath()); availableFileSystems.put("cache-external", context.getExternalCacheDir().getAbsolutePath()); } catch (NullPointerException e) { - LOG.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on"); + Log.d(LOG_TAG, "External storage unavailable, check to see if USB Mass Storage Mode is on"); } } @@ -167,8 +165,8 @@ protected HashMap getAvailableFileSystems(Activity activity) { } @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.filesystems = new ArrayList(); this.pendingRequests = new PendingRequests(); @@ -225,7 +223,7 @@ public void initialize(Host host, AppView webView) { FileUtils.filePlugin = this; } } else { - LOG.e(LOG_TAG, "File plugin configuration error: Please set AndroidPersistentFileLocation in config.xml to one of \"internal\" (for new applications) or \"compatibility\" (for compatibility with previous versions)"); + Log.e(LOG_TAG, "File plugin configuration error: Please set AndroidPersistentFileLocation in config.xml to one of \"internal\" (for new applications) or \"compatibility\" (for compatibility with previous versions)"); activity.finish(); } } @@ -536,17 +534,17 @@ public void run(JSONArray args) throws FileNotFoundException, JSONException, Mal private void getReadPermission(String rawArgs, int action, Callback callbackContext) { int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - PermissionHelper.requestPermissions(this, requestCode, + host.requestPermissions(this, requestCode, new String[]{Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_AUDIO}); } else { - PermissionHelper.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE); + host.requestPermission(this, requestCode, Manifest.permission.READ_EXTERNAL_STORAGE); } } private void getWritePermission(String rawArgs, int action, Callback callbackContext) { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { int requestCode = pendingRequests.createRequest(rawArgs, action, callbackContext); - PermissionHelper.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE); + host.requestPermission(this, requestCode, Manifest.permission.WRITE_EXTERNAL_STORAGE); } } @@ -561,11 +559,11 @@ private void getWritePermission(String rawArgs, int action, Callback callbackCon */ private boolean hasReadPermission() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return PermissionHelper.hasPermission(this, Manifest.permission.READ_MEDIA_IMAGES) - && PermissionHelper.hasPermission(this, Manifest.permission.READ_MEDIA_VIDEO) - && PermissionHelper.hasPermission(this, Manifest.permission.READ_MEDIA_AUDIO); + return host.hasPermission(Manifest.permission.READ_MEDIA_IMAGES) + && host.hasPermission(Manifest.permission.READ_MEDIA_VIDEO) + && host.hasPermission(Manifest.permission.READ_MEDIA_AUDIO); } else { - return PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); + return host.hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE); } } @@ -573,7 +571,7 @@ private boolean hasWritePermission() { // Starting with API 33, requesting WRITE_EXTERNAL_STORAGE is an auto permission rejection return android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ? true - : PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + : host.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); } private boolean needPermission(String nativeURL, int permissionType) throws JSONException { @@ -1007,7 +1005,7 @@ private JSONObject requestAllPaths() throws JSONException { ret.put("externalRootDirectory", toDirUrl(Environment.getExternalStorageDirectory())); } catch (NullPointerException e) { /* If external storage is unavailable, context.getExternal* returns null */ - LOG.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage"); + Log.d(LOG_TAG, "Unable to access these paths, most liklely due to USB storage"); } } return ret; @@ -1105,7 +1103,7 @@ public void handleData(InputStream inputStream, String contentType) { callbackContext.sendPayload(result); } catch (IOException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); + Log.d(LOG_TAG, e.getLocalizedMessage()); callbackContext.sendPayload(new Payload(Payload.Status.IO_EXCEPTION, NOT_READABLE_ERR)); } } @@ -1119,7 +1117,7 @@ public void handleData(InputStream inputStream, String contentType) { } catch (FileNotFoundException e) { callbackContext.sendPayload(new Payload(Payload.Status.IO_EXCEPTION, NOT_FOUND_ERR)); } catch (IOException e) { - LOG.d(LOG_TAG, e.getLocalizedMessage()); + Log.d(LOG_TAG, e.getLocalizedMessage()); callbackContext.sendPayload(new Payload(Payload.Status.IO_EXCEPTION, NOT_READABLE_ERR)); } } @@ -1231,7 +1229,7 @@ public void run(JSONArray args) throws FileNotFoundException, JSONException, Mal break; } } else { - LOG.d(LOG_TAG, "Received permission callback for unknown request code"); + Log.d(LOG_TAG, "Received permission callback for unknown request code"); } } diff --git a/platforms/android/app/src/main/java/app/acode/file/Filesystem.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/Filesystem.java similarity index 99% rename from platforms/android/app/src/main/java/app/acode/file/Filesystem.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/Filesystem.java index d6c787a..f7d15d5 100644 --- a/platforms/android/app/src/main/java/app/acode/file/Filesystem.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/Filesystem.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.net.Uri; @@ -29,8 +29,8 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.ArrayList; import java.util.Arrays; -import runner.Preferences; -import runner.ResourceApi; +import com.foxdebug.acode.runtime.Preferences; +import com.foxdebug.acode.runtime.ResourceApi; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/app/acode/file/InvalidModificationException.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/InvalidModificationException.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/InvalidModificationException.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/InvalidModificationException.java index 59e791d..da9d9dc 100644 --- a/platforms/android/app/src/main/java/app/acode/file/InvalidModificationException.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/InvalidModificationException.java @@ -18,7 +18,7 @@ Licensed to the Apache Software Foundation (ASF) under one */ -package app.acode.file; +package com.foxdebug.acode.file; @SuppressWarnings("serial") public class InvalidModificationException extends Exception { diff --git a/platforms/android/app/src/main/java/app/acode/file/LocalFilesystem.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystem.java similarity index 99% rename from platforms/android/app/src/main/java/app/acode/file/LocalFilesystem.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystem.java index bad03f2..f7d6902 100644 --- a/platforms/android/app/src/main/java/app/acode/file/LocalFilesystem.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystem.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.content.Context; import android.content.Intent; @@ -25,8 +25,8 @@ Licensed to the Apache Software Foundation (ASF) under one import android.os.Environment; import android.util.Base64; -import runner.Preferences; -import runner.ResourceApi; +import com.foxdebug.acode.runtime.Preferences; +import com.foxdebug.acode.runtime.ResourceApi; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/app/acode/file/LocalFilesystemURL.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystemURL.java similarity index 98% rename from platforms/android/app/src/main/java/app/acode/file/LocalFilesystemURL.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystemURL.java index 37760ce..37df05e 100644 --- a/platforms/android/app/src/main/java/app/acode/file/LocalFilesystemURL.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/LocalFilesystemURL.java @@ -16,7 +16,7 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.net.Uri; diff --git a/platforms/android/app/src/main/java/app/acode/file/NoModificationAllowedException.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/NoModificationAllowedException.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/NoModificationAllowedException.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/NoModificationAllowedException.java index 40a5dc4..bbe2cd2 100644 --- a/platforms/android/app/src/main/java/app/acode/file/NoModificationAllowedException.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/NoModificationAllowedException.java @@ -17,7 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; @SuppressWarnings("serial") public class NoModificationAllowedException extends Exception { diff --git a/platforms/android/app/src/main/java/app/acode/file/PendingRequests.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/PendingRequests.java similarity index 97% rename from platforms/android/app/src/main/java/app/acode/file/PendingRequests.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/PendingRequests.java index eadfd81..c00cc44 100644 --- a/platforms/android/app/src/main/java/app/acode/file/PendingRequests.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/PendingRequests.java @@ -16,11 +16,11 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ -package app.acode.file; +package com.foxdebug.acode.file; import android.util.SparseArray; -import runner.Callback; +import com.foxdebug.acode.runtime.Callback; /** * Holds pending runtime permission requests diff --git a/platforms/android/app/src/main/java/app/acode/file/TypeMismatchException.java b/platforms/android/app/src/main/java/com/foxdebug/acode/file/TypeMismatchException.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/file/TypeMismatchException.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/file/TypeMismatchException.java index ac274e3..e1ab6cb 100644 --- a/platforms/android/app/src/main/java/app/acode/file/TypeMismatchException.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/file/TypeMismatchException.java @@ -18,7 +18,7 @@ Licensed to the Apache Software Foundation (ASF) under one */ -package app.acode.file; +package com.foxdebug.acode.file; @SuppressWarnings("serial") public class TypeMismatchException extends Exception { diff --git a/platforms/android/app/src/main/java/com/foxdebug/ftp/Ftp.java b/platforms/android/app/src/main/java/com/foxdebug/acode/ftp/Ftp.java similarity index 96% rename from platforms/android/app/src/main/java/com/foxdebug/ftp/Ftp.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/ftp/Ftp.java index d6fa605..6ae42ce 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/ftp/Ftp.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/ftp/Ftp.java @@ -1,40 +1,32 @@ -package com.foxdebug.ftp; +package com.foxdebug.acode.ftp; import android.app.Activity; import android.content.Context; import android.util.Log; import java.io.File; 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 java.io.PrintWriter; -import java.io.UnsupportedEncodingException; import java.lang.SecurityException; import java.lang.reflect.Method; -import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; + import org.apache.commons.net.ftp.*; import org.apache.commons.net.ftp.parser.ParserInitializationException; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; + +import com.foxdebug.acode.Acode; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; +import kotlinx.coroutines.GlobalScope; + public class Ftp extends Service { HashMap ftpProfiles = new HashMap(); @@ -42,10 +34,10 @@ public class Ftp extends Service { Activity activity; String connectionID; - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); - context = host.getContext(); - activity = host.getActivity(); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); + context = bridgeContext.getWebActivity(); + activity = bridgeContext.getWebActivity(); } public boolean execute( @@ -83,7 +75,7 @@ public void connect( Callback callback, boolean isRetry ) { - host + Acode.getInstance() .getThreadPool() .execute( new Runnable() { @@ -203,7 +195,7 @@ private static String getParentPath(String path) { } public void listDirectory(JSONArray args, Callback callback) { - host + Acode.getInstance() .getThreadPool() .execute( new Runnable() { @@ -316,8 +308,8 @@ public void run() { } public void exists(JSONArray args, Callback callback) { - host - .getThreadPool() + + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -366,8 +358,7 @@ public void run() { } public void sendNoOp(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -389,8 +380,7 @@ public void run() { } public void deleteFile(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -429,8 +419,7 @@ public void run() { } public void deleteDirectory(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -472,8 +461,7 @@ public void run() { } public void rename(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -559,8 +547,7 @@ public void run() { } public void downloadFile(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -646,8 +633,7 @@ public void run() { } public void uploadFile(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -724,8 +710,7 @@ public void run() { } public void getKeepAlive(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -753,8 +738,7 @@ public void run() { } public void execCommand(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -792,8 +776,7 @@ public void run() { } public void isConnected(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -823,8 +806,7 @@ public void run() { } public void disconnect(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -847,8 +829,7 @@ public void run() { } public void createDirectory(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -885,8 +866,7 @@ public void run() { } public void changeDirectory(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -926,8 +906,7 @@ public void changeToParentDirectory( JSONArray args, Callback callback ) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -958,8 +937,7 @@ public void run() { } public void getWorkingDirectory(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { @@ -990,8 +968,7 @@ public void run() { } public void getStat(JSONArray args, Callback callback) { - host - .getThreadPool() + Acode.getInstance().getThreadPool() .execute( new Runnable() { public void run() { diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeClientAuth.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeClientAuth.java similarity index 97% rename from platforms/android/app/src/main/java/app/acode/http/NativeClientAuth.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeClientAuth.java index 7f164f2..c72f001 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeClientAuth.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeClientAuth.java @@ -1,4 +1,4 @@ -package app.acode.http; +package com.foxdebug.acode.http; import android.app.Activity; import android.content.Context; @@ -16,7 +16,7 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; -import runner.Callback; +import com.foxdebug.acode.runtime.Callback; import com.silkimen.http.KeyChainKeyManager; import com.silkimen.http.TLSConfiguration; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpBase.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpBase.java similarity index 99% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpBase.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpBase.java index d58211e..50090f1 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpBase.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpBase.java @@ -1,16 +1,13 @@ -package app.acode.http; +package com.foxdebug.acode.http; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; -import java.nio.ByteBuffer; - import javax.net.ssl.SSLException; import com.silkimen.http.HttpBodyDecoder; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpDownload.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpDownload.java similarity index 90% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpDownload.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpDownload.java index 277950d..223bfa5 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpDownload.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpDownload.java @@ -1,15 +1,12 @@ -package app.acode.http; +package com.foxdebug.acode.http; import java.io.File; import java.net.URI; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; - import com.silkimen.http.HttpRequest; import com.silkimen.http.TLSConfiguration; -import app.acode.file.FileUtils; +import com.foxdebug.acode.file.FileUtils; import org.json.JSONObject; class NativeHttpDownload extends NativeHttpBase { diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpOperation.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpOperation.java similarity index 90% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpOperation.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpOperation.java index 3a90c22..9b97e53 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpOperation.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpOperation.java @@ -1,7 +1,4 @@ -package app.acode.http; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; +package com.foxdebug.acode.http; import com.silkimen.http.TLSConfiguration; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpPlugin.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpPlugin.java similarity index 97% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpPlugin.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpPlugin.java index e45608d..16f7642 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpPlugin.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpPlugin.java @@ -1,4 +1,4 @@ -package app.acode.http; +package com.foxdebug.acode.http; import java.security.KeyStore; import java.util.HashMap; @@ -8,10 +8,9 @@ import com.silkimen.http.TLSConfiguration; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -33,8 +32,8 @@ public class NativeHttpPlugin extends Service implements Observer { private final Object reqMapLock = new Object(); @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.tlsConfiguration = new TLSConfiguration(); diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpResponse.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpResponse.java similarity index 98% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpResponse.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpResponse.java index 75b9fcb..4f3e8bf 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpResponse.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpResponse.java @@ -1,4 +1,4 @@ -package app.acode.http; +package com.foxdebug.acode.http; import java.nio.ByteBuffer; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeHttpUpload.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpUpload.java similarity index 96% rename from platforms/android/app/src/main/java/app/acode/http/NativeHttpUpload.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpUpload.java index 2fe5013..a13178d 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeHttpUpload.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeHttpUpload.java @@ -1,4 +1,4 @@ -package app.acode.http; +package com.foxdebug.acode.http; import android.content.ContentResolver; import android.content.Context; @@ -14,9 +14,6 @@ import java.io.InputStream; import java.net.URI; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; - import org.json.JSONArray; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeObservableCallbackContext.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeObservableCallbackContext.java similarity index 94% rename from platforms/android/app/src/main/java/app/acode/http/NativeObservableCallbackContext.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeObservableCallbackContext.java index d5b5a28..c40f238 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeObservableCallbackContext.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeObservableCallbackContext.java @@ -1,6 +1,6 @@ -package app.acode.http; +package com.foxdebug.acode.http; -import runner.Callback; +import com.foxdebug.acode.runtime.Callback; import org.json.JSONObject; import java.util.Observer; diff --git a/platforms/android/app/src/main/java/app/acode/http/NativeServerTrust.java b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeServerTrust.java similarity index 98% rename from platforms/android/app/src/main/java/app/acode/http/NativeServerTrust.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeServerTrust.java index 2425672..27b578e 100644 --- a/platforms/android/app/src/main/java/app/acode/http/NativeServerTrust.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/http/NativeServerTrust.java @@ -1,4 +1,4 @@ -package app.acode.http; +package com.foxdebug.acode.http; import java.io.IOException; import java.security.GeneralSecurityException; @@ -8,7 +8,7 @@ import com.silkimen.http.TLSConfiguration; -import runner.Callback; +import com.foxdebug.acode.runtime.Callback; import android.app.Activity; import android.util.Log; diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/App.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/App.kt new file mode 100644 index 0000000..9175430 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/App.kt @@ -0,0 +1,105 @@ +package com.foxdebug.acode.runtime + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import org.json.JSONArray +import org.json.JSONException +import java.util.concurrent.ExecutorService + +class App : Service() { + private lateinit var bridgeContext: BridgeContext + + override fun initialize(bridgeContext: BridgeContext) { + super.initialize(bridgeContext) + this.bridgeContext = bridgeContext + } + + @Throws(JSONException::class) + override fun execute(action: String, args: JSONArray, callback: Callback): Boolean { + val webView = bridgeContext.webView + when (action) { + "exitApp" -> host!!.activity.runOnUiThread(Runnable { host!!.activity.finish() }) + "overrideButton" -> host!!.activity + .overrideButton(args.getString(0), args.getBoolean(1)) + + "clearCache" -> webView.post(Runnable { webView.clearCache(true) }) + "clearHistory" -> webView.post(Runnable { webView.clearHistory() }) + "backHistory" -> webView.post(Runnable { webView.goBack() }) + else -> return false + } + callback.success() + return true + } +} + +class Host(val activity: BaseWebActivity) { + val preferences: Preferences = Preferences() + private val permissionRequests: MutableMap = HashMap() + private val activityRequests: MutableMap = HashMap() + private var requestId = 40000 + + init { + preferences.set("AndroidPersistentFileLocation", "Compatibility") + preferences.set("AndroidBlacklistSecureSocketProtocols", "SSLv3,TLSv1") + preferences.set("scheme", "https") + preferences.set("hostname", "localhost") + preferences.set("BackgroundColor", -0xcececf) + } + + val context: Context + get() = activity + + val threadPool: ExecutorService? + get() = RuntimeBaseApplication.instance!!.threadPool + + fun hasPermission(permission: String): Boolean { + return (ActivityCompat.checkSelfPermission(activity, permission) == + PackageManager.PERMISSION_GRANTED + ) + } + + fun requestPermission(service: Service, code: Int, permission: String?) { + requestPermissions(service, code, arrayOf(permission)) + } + + fun requestPermissions( + service: Service, + code: Int, + permissions: Array + ) { + activity.runOnUiThread(Runnable { + val id = requestId++ + permissionRequests[id] = Request(service, code) + ActivityCompat.requestPermissions(activity, permissions, id) + }) + } + + fun startActivityForResult(service: Service, intent: Intent, code: Int) { + activity.runOnUiThread(Runnable { + val id = requestId++ + activityRequests[id] = Request(service, code) + activity.startActivityForResult(intent, id) + }) + } + + @Throws(JSONException::class) + fun onPermissionsResult(id: Int, permissions: Array?, grants: IntArray?): Boolean { + val request = permissionRequests.remove(id) ?: return false + request.service.onRequestPermissionResult( + request.code, + permissions, + grants + ) + return true + } + + fun onActivityResult(id: Int, result: Int, data: Intent?): Boolean { + val request = activityRequests.remove(id) ?: return false + request.service.onActivityResult(request.code, result, data) + return true + } + + private class Request(val service: Service, val code: Int) +} \ No newline at end of file diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/BaseWebActivity.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/BaseWebActivity.kt new file mode 100644 index 0000000..282ae6e --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/BaseWebActivity.kt @@ -0,0 +1,242 @@ +package com.foxdebug.acode.runtime + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.KeyEvent +import android.view.View +import android.webkit.CookieManager +import android.webkit.WebView +import android.widget.FrameLayout +import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import androidx.appcompat.app.AppCompatActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.foxdebug.acode.BuildConfig +import com.foxdebug.acode.runtime.webview.AppWebView +import com.foxdebug.acode.runtime.webview.AcodeChromeClient +import com.foxdebug.acode.runtime.webview.AcodeWebViewClient +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max + +open class BaseWebActivity : AppCompatActivity() { + private var appWebView: AppWebView? = null + private var host: Host? = null + private var bridge: Bridge? = null + private var acodeChromeClient: AcodeChromeClient? = null + private var content: FrameLayout? = null + private var hasPaused = false + private val overriddenButtons: MutableSet = ConcurrentHashMap.newKeySet() + + @SuppressLint("SetJavaScriptEnabled") + public override fun onCreate(state: Bundle?) { + installSplashScreen() + super.onCreate(state) + context = WeakReference(this) + WindowCompat.setDecorFitsSystemWindows(window, false) + host = Host(this) + content = FrameLayout(this) + appWebView = AppWebView(this) + bridge = Bridge(appWebView!!,this) + appWebView!!.initialize(bridge!!) + acodeChromeClient = AcodeChromeClient(this, appWebView!!) + appWebView!!.webViewClient = AcodeWebViewClient(this, bridge!!) + appWebView!!.webChromeClient = acodeChromeClient!! + appWebView!!.setFullscreenController(acodeChromeClient!!) + appWebView!!.addJavascriptInterface(bridge!!, "Android") + appWebView!!.isVerticalScrollBarEnabled = false + appWebView!!.settings.javaScriptEnabled = true + appWebView!!.settings.javaScriptCanOpenWindowsAutomatically = true + appWebView!!.settings.setSaveFormData(false) + appWebView!!.settings.setGeolocationEnabled(true) + CookieManager.getInstance().setAcceptThirdPartyCookies( + appWebView, + true + ) + appWebView!!.settings.domStorageEnabled = true + appWebView!!.settings.setDatabaseEnabled(true) + appWebView!!.settings.allowFileAccess = true + appWebView!!.settings.allowContentAccess = true + appWebView!!.settings.mediaPlaybackRequiresUserGesture = false + appWebView!!.overScrollMode = View.OVER_SCROLL_NEVER + appWebView!!.setBackgroundColor(-0xcececf) + WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) + content!!.addView(appWebView, FrameLayout.LayoutParams(-1, -1)) + val statusBar = View(this) + statusBar.tag = "statusBarView" + content!!.addView(statusBar) + setContentView(content) + ViewCompat.setOnApplyWindowInsetsListener( + content!!, + OnApplyWindowInsetsListener { view: View?, insets: WindowInsetsCompat? -> + val bars = insets!!.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + val keyboard = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + val fullscreen = acodeChromeClient!!.isFullscreen + val top = + if (!fullscreen && statusBar.visibility != View.GONE) bars.top else 0 + val params = + appWebView!!.layoutParams as FrameLayout.LayoutParams + val left = if (fullscreen) 0 else bars.left + val right = if (fullscreen) 0 else bars.right + val bottom = if (fullscreen) 0 else max(bars.bottom, keyboard) + if (params.leftMargin != left || params.topMargin != top || params.rightMargin != right || params.bottomMargin != bottom + ) { + params.setMargins(left, top, right, bottom) + appWebView!!.layoutParams = params + } + statusBar.layoutParams = FrameLayout.LayoutParams(-1, top, Gravity.TOP) + insets + }) + + OnBackPressedDispatcher().addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + dispatchBack() + } + } + ) + bridge!!.initialize() + appWebView!!.loadUrl("https://localhost/index.html") + } + + fun overrideButton(button: String, enabled: Boolean) { + var button = button + if (button == "volumeup" || button == "volumedown") button += + "button" + if (enabled) overriddenButtons.add(button) + else overriddenButtons.remove(button) + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (appWebView == null) return super.dispatchKeyEvent(event) + val code = event.keyCode + if (code == KeyEvent.KEYCODE_BACK && + (acodeChromeClient!!.isFullscreen || overriddenButtons.contains("backbutton")) + ) { + if (event.action == KeyEvent.ACTION_UP) dispatchBack() + return true + } + val name = + if (code == KeyEvent.KEYCODE_MENU) + "menubutton" + else + if (code == KeyEvent.KEYCODE_VOLUME_UP) + "volumeupbutton" + else + if (code == KeyEvent.KEYCODE_VOLUME_DOWN) + "volumedownbutton" + else + null + if (name != null && overriddenButtons.contains(name)) { + if (event.action == KeyEvent.ACTION_UP + ) appWebView!!.fireDocumentEvent(name) + return true + } + return super.dispatchKeyEvent(event) + } + + private fun dispatchBack() { + if (acodeChromeClient!!.handleBack()) return + if (overriddenButtons.contains("backbutton")) appWebView!!.fireDocumentEvent( + "backbutton" + ) + else if (appWebView!!.canGoBack()) appWebView!!.goBack() + else finish() + } + + fun getHost(): Host { + return host!! + } + + val contentView: FrameLayout + get() = content!! + + override fun onConfigurationChanged( + configuration: Configuration + ) { + super.onConfigurationChanged(configuration) + for (service in bridge!!.activeServices) service.onConfigurationChanged(configuration) + } + + public override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + setIntent(intent) + for (service in bridge!!.activeServices) service.onNewIntent(intent) + } + + override fun onPause() { + super.onPause() + if (appWebView == null) return + hasPaused = true + appWebView!!.fireDocumentEvent("pause") + for (service in bridge!!.activeServices) service.onPause(true) + acodeChromeClient!!.pause() + } + + override fun onResume() { + super.onResume() + if (appWebView == null) return + appWebView!!.onResume() + appWebView!!.resumeTimers() + acodeChromeClient!!.resume() + for (service in bridge!!.activeServices) service.onResume(true) + if (hasPaused) appWebView!!.fireDocumentEvent("resume") + } + + override fun onActivityResult(code: Int, result: Int, data: Intent?) { + super.onActivityResult(code, result, data) + if (!host!!.onActivityResult(code, result, data)) { + for (service in bridge!!.activeServices) service.onActivityResult(code, result, data) + } + } + + override fun onRequestPermissionsResult( + code: Int, + permissions: Array, + grants: IntArray + ) { + super.onRequestPermissionsResult(code, permissions, grants) + try { + if (!host!!.onPermissionsResult(code, permissions, grants)) { + for (service in bridge!!.activeServices) service.onRequestPermissionResult( + code, + permissions, + grants + ) + } + } catch (exception: JSONException) { + Log.e("Acode", "Permission callback failed", exception) + } + } + + override fun onDestroy() { + if (bridge != null) bridge!!.destroy() + if (acodeChromeClient != null) acodeChromeClient!!.reset() + if (appWebView != null) { + content!!.removeView(appWebView) + appWebView!!.destroy() + } + super.onDestroy() + } + + companion object { + private var context: WeakReference? = null + fun getContext(): Context? { + return if (context == null) null else context!!.get() + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Bridge.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Bridge.kt new file mode 100644 index 0000000..8aabd9b --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Bridge.kt @@ -0,0 +1,112 @@ +package com.foxdebug.acode.runtime + +import android.net.Uri +import android.util.Log +import android.webkit.JavascriptInterface +import com.foxdebug.acode.runtime.RuntimeBaseApplication.Companion.instance +import com.foxdebug.acode.runtime.webview.AppWebView +import kotlin.collections.LinkedHashMap + +data class BridgeContext(val webView: AppWebView,val webActivity: BaseWebActivity) + +class Bridge(private val webView: AppWebView, private val webActivity: BaseWebActivity) { + private val definitions: Map = + ServiceRegistry.definitions.associateBy { definition -> definition.name } + + private val definitionsByKey: Map = + ServiceRegistry.definitions.associateBy { definition -> definition.name.key } + + private val services: MutableMap = LinkedHashMap() + + @Volatile + var generation: Long = 0 + private set + + /** Services created so far, in the order they were requested. */ + val activeServices: List + get() = services.values.toList() + + fun initialize() { + definitions.values + .filter { definition -> definition.loadOnStart } + .forEach { definition -> getService(definition) } + } + + @Synchronized + fun getService(definition: ServiceDefinition): Service { + return services.getOrPut(definition.name.key) { + definition.factory.create().also { service -> + service.initialize(BridgeContext(webView, webActivity)) + service.serviceInitialize() + } + } + } + + @Synchronized + fun getService(name: ServiceName): Service? { + return definitions[name]?.let { definition -> getService(definition) } + } + + @JavascriptInterface + fun exec(name: String?, action: String, args: String, id: Long): Boolean { + val callback = Callback(id, webView) + val definition = name?.let { definitionsByKey[it] } + if (definition == null) { + callback.error("Unknown native service: $name") + return true + } + val requestGeneration = generation + webView.post { + if (generation != requestGeneration) return@post + try { + val service = getService(definition) + instance!! + .threadPoolSingle!! + .execute { + if (generation != requestGeneration) return@execute + try { + if (!service.execute(action, args, callback)) { + callback.sendPayload( + Payload(Payload.Status.INVALID_ACTION, action) + ) + } + } catch (exception: Exception) { + Log.e(TAG, "Service $name failed to execute $action", exception) + callback.error(exception.toString()) + } + } + } catch (exception: Exception) { + Log.e(TAG, "Unable to run service $name", exception) + callback.error(exception.toString()) + } + } + return true + } + + fun remapUri(uri: Uri?): Uri? { + for (service in activeServices) { + val result = service.remapUri(uri) + if (result != null) return result + } + return null + } + + fun postMessage(name: String?, value: Any?) { + for (service in activeServices) service.onMessage(name, value) + } + + fun reset() { + generation++ + for (service in activeServices) service.onReset() + } + + fun destroy() { + generation++ + for (service in activeServices) service.onDestroy() + services.clear() + } + + companion object { + private const val TAG = "Bridge" + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Callback.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Callback.kt new file mode 100644 index 0000000..1f2fc3d --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Callback.kt @@ -0,0 +1,50 @@ +package com.foxdebug.acode.runtime + +import android.util.Log +import com.foxdebug.acode.runtime.webview.AppWebView +import org.json.JSONException + +open class Callback(private val id: Long, private val webView: AppWebView) { + private val generation: Long = webView.bridge!!.generation + + @get:Synchronized + var isFinished: Boolean = false + private set + + val callbackId: String + get() = id.toString() + + fun success() { + sendPayload(Payload(Payload.Status.OK)) + } + + fun success(data: Any?) { + sendPayload(Payload(Payload.Status.OK, data)) + } + + fun error(data: Any?) { + sendPayload(Payload(Payload.Status.ERROR, data)) + } + + @Synchronized + open fun sendPayload(payload: Payload) { + if (this.isFinished) return + this.isFinished = !payload.keepCallback + if (payload.getStatus() == Payload.Status.NO_RESULT.ordinal && + payload.keepCallback + ) return + try { + val json = payload.toJSON(id, generation).toString() + webView.post(Runnable { + if (generation == webView.bridge!!.generation) { + webView.evaluateJavascript( + "window.Android.callback($json);", + null + ) + } + }) + } catch (exception: JSONException) { + Log.e("Acode", "Unable to serialize callback", exception) + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Payload.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Payload.kt new file mode 100644 index 0000000..f314e6f --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Payload.kt @@ -0,0 +1,90 @@ +package com.foxdebug.acode.runtime + +import android.util.Base64 +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class Payload @JvmOverloads constructor( + private val status: Status, + private val data: Any? = STATUS_MESSAGES[status.ordinal] +) { + enum class Status { + NO_RESULT, + OK, + CLASS_NOT_FOUND_EXCEPTION, + ILLEGAL_ACCESS_EXCEPTION, + INSTANTIATION_EXCEPTION, + MALFORMED_URL_EXCEPTION, + IO_EXCEPTION, + INVALID_ACTION, + JSON_EXCEPTION, + ERROR, + } + + var keepCallback: Boolean = false + + constructor(status: Status, data: ByteArray?, binaryString: Boolean) : this( + status, + encodeBinary(data, binaryString) + ) + + constructor(data: Any?) : this(Status.OK, data) + + fun getStatus(): Int { + return status.ordinal + } + + @Throws(JSONException::class) + fun toJSON(id: Long, generation: Long): JSONObject { + val result = JSONObject() + result.put("id", id) + result.put("generation", generation) + result.put("type", if (status == Status.OK) 0 else 1) + result.put("status", status.ordinal) + result.put("keep", this.keepCallback) + result.put("data", encode(data)) + return result + } + + companion object { + const val MESSAGE_TYPE_STRING: Int = 1 + const val MESSAGE_TYPE_ARRAYBUFFER: Int = 6 + const val MESSAGE_TYPE_BINARYSTRING: Int = 7 + + private val STATUS_MESSAGES = arrayOf( + "No result", + "OK", + "Class not found", + "Illegal access", + "Instantiation error", + "Malformed url", + "IO error", + "Invalid action", + "JSON error", + "Error", + ) + + @Throws(JSONException::class) + private fun encode(value: Any?): Any { + if (value == null) return JSONObject.NULL + if (value is ByteArray) return encodeBinary(value, false) + if (value is MutableList<*>) { + val parts = JSONArray() + for (part in value) parts.put(encode((part as Payload).data)) + return JSONObject().put("kind", "multipart").put("data", parts) + } + return value + } + + private fun encodeBinary(bytes: ByteArray?, binaryString: Boolean): JSONObject { + try { + return JSONObject() + .put("kind", if (binaryString) "binaryString" else "arrayBuffer") + .put("data", Base64.encodeToString(bytes, Base64.NO_WRAP)) + } catch (exception: JSONException) { + throw IllegalArgumentException(exception) + } + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Preferences.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Preferences.kt new file mode 100644 index 0000000..b8b2084 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Preferences.kt @@ -0,0 +1,97 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ +package com.foxdebug.acode.runtime + +import java.lang.Long +import kotlin.Boolean +import kotlin.Double +import kotlin.Int +import kotlin.String + +class Preferences { + private val prefs = HashMap(20) + + fun set(name: String, value: String?) { + prefs[name.lowercase()] = value + } + + fun set(name: String, value: Boolean) { + set(name, "" + value) + } + + fun set(name: String, value: Int) { + set(name, "" + value) + } + + fun set(name: String, value: Double) { + set(name, "" + value) + } + + val all: MutableMap + get() = prefs + + fun getBoolean(name: String, defaultValue: Boolean): Boolean { + var name = name + name = name.lowercase() + val value = prefs.get(name) + if (value != null) { + return value.toBoolean() + } + return defaultValue + } + + // Added in 4.0.0 + fun contains(name: String): Boolean { + return getString(name, null) != null + } + + fun getInteger(name: String, defaultValue: Int): Int { + var name = name + name = name.lowercase() + val value = prefs[name] + if (value != null) { + // Some 32-bit hex values (for example, 0x80000000) are valid int bit patterns + // but exceed Integer.MAX_VALUE when read as positive numbers. Integer.decode() + // rejects such values with NumberFormatException, so decode as long first and + // cast to int to preserve the intended 32-bit value. + return (Long.decode(value) as kotlin.Long).toInt() + } + return defaultValue + } + + fun getDouble(name: String, defaultValue: Double): Double { + var name = name + name = name.lowercase() + val value = prefs[name] + if (value != null) { + return value.toDouble() + } + return defaultValue + } + + fun getString(name: String, defaultValue: String?): String? { + var name = name + name = name.lowercase() + val value = prefs[name] + if (value != null) { + return value + } + return defaultValue + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ResourceApi.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ResourceApi.kt new file mode 100644 index 0000000..b15107c --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ResourceApi.kt @@ -0,0 +1,433 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ +package com.foxdebug.acode.runtime + +import android.content.ContentResolver +import android.content.Context +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.net.Uri +import android.os.Looper +import android.util.Base64 +import android.webkit.MimeTypeMap +import java.io.ByteArrayInputStream +import java.io.File +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 java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.URL +import java.util.Locale +import java.util.zip.GZIPInputStream + + +class ResourceApi(context: Context, private val pluginManager: Bridge) { + private val assetManager: AssetManager = context.assets + private val contentResolver: ContentResolver = context.contentResolver + var isThreadCheckingEnabled: Boolean = true + + + fun remapUri(uri: Uri): Uri { + assertNonRelative(uri) + val pluginUri = pluginManager.remapUri(uri) + return pluginUri ?: uri + } + + fun remapPath(path: String): String? { + return remapUri(Uri.fromFile(File(path))).path + } + + /** + * @return A file that points to the resource, or null if the resource is not on the local filesystem. + */ + fun mapUriToFile(uri: Uri): File? { + assertBackgroundThread() + when (getUriType(uri)) { + URI_TYPE_FILE -> return File(uri.path!!) + URI_TYPE_CONTENT -> { + val cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null) + cursor?.use { cursor -> + val columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]) + if (columnIndex != -1 && cursor.count > 0) { + cursor.moveToFirst() + val realPath = cursor.getString(columnIndex) + if (realPath != null) { + return File(realPath) + } + } + } + } + } + return null + } + + fun getMimeType(uri: Uri): String? { + when (getUriType(uri)) { + URI_TYPE_FILE, URI_TYPE_ASSET -> return getMimeTypeFromPath(uri.path!!) + URI_TYPE_CONTENT, URI_TYPE_RESOURCE -> return contentResolver.getType(uri) + URI_TYPE_DATA -> { + return getDataUriMimeType(uri) + } + + URI_TYPE_HTTP, URI_TYPE_HTTPS -> { + try { + val conn = URL(uri.toString()).openConnection() as HttpURLConnection + conn.setDoInput(false) + conn.requestMethod = "HEAD" + var mimeType = conn.getHeaderField("Content-Type") + if (mimeType != null) { + mimeType = mimeType.split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + } + return mimeType + } catch (e: IOException) { + } + } + } + + return null + } + + + //This already exists + private fun getMimeTypeFromPath(path: String): String? { + var extension = path + val lastDot = extension.lastIndexOf('.') + if (lastDot != -1) { + extension = extension.substring(lastDot + 1) + } + // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185). + extension = extension.lowercase(Locale.getDefault()) + if (extension == "3ga") { + return "audio/3gpp" + } else if (extension == "js") { + // Missing from the map :(. + return "text/javascript" + } + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + + /** + * Opens a stream to the given URI, also providing the MIME type & length. + * + * @return Never returns null. + * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before + * being passed into this function. + * @throws IOException If the URI cannot be opened. + * @throws IllegalStateException If called on a foreground thread and skipThreadCheck is false. + */ + /** + * Opens a stream to the given URI, also providing the MIME type & length. + * + * @return Never returns null. + * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before + * being passed into this function. + * @throws IOException If the URI cannot be opened. + * @throws IllegalStateException If called on a foreground thread. + */ + @JvmOverloads + @Throws(IOException::class) + fun openForRead(uri: Uri, skipThreadCheck: Boolean = false): OpenForReadResult? { + if (!skipThreadCheck) { + assertBackgroundThread() + } + when (getUriType(uri)) { + URI_TYPE_FILE -> { + val inputStream = FileInputStream(uri.path) + val mimeType = getMimeTypeFromPath(uri.path!!) + val length = inputStream.channel.size() + return OpenForReadResult(uri, inputStream, mimeType, length, null) + } + + URI_TYPE_ASSET -> { + val assetPath = uri.path!!.substring(15) + var assetFd: AssetFileDescriptor? = null + var inputStream: InputStream + var length: Long = -1 + try { + assetFd = assetManager.openFd(assetPath) + inputStream = assetFd.createInputStream() + length = assetFd.length + } catch (e: FileNotFoundException) { + // Will occur if the file is compressed. + inputStream = assetManager.open(assetPath) + length = inputStream.available().toLong() + } + val mimeType = getMimeTypeFromPath(assetPath) + return OpenForReadResult(uri, inputStream, mimeType, length, assetFd) + } + + URI_TYPE_CONTENT, URI_TYPE_RESOURCE -> { + val mimeType = contentResolver.getType(uri) + val assetFd = contentResolver.openAssetFileDescriptor(uri, "r") + val inputStream: InputStream = assetFd!!.createInputStream() + val length = assetFd.length + return OpenForReadResult(uri, inputStream, mimeType, length, assetFd) + } + + URI_TYPE_DATA -> { + val ret = readDataUri(uri) + return ret + } + + URI_TYPE_HTTP, URI_TYPE_HTTPS -> { + val conn = URL(uri.toString()).openConnection() as HttpURLConnection + conn.setRequestProperty("Accept-Encoding", "gzip") + conn.setDoInput(true) + var mimeType = conn.getHeaderField("Content-Type") + if (mimeType != null) { + mimeType = mimeType.split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + } + val length = conn.contentLength + val inputStream: InputStream + if ("gzip" == conn.contentEncoding) { + inputStream = GZIPInputStream(conn.getInputStream()) + } else { + inputStream = conn.getInputStream() + } + return OpenForReadResult(uri, inputStream, mimeType, length.toLong(), null) + } + + URI_TYPE_PLUGIN -> { + //wtf is this? + //i am commenting this for now + //probably break uri stuff + +// val plugin = ServiceName.fromKey(uri.host)?.let { name -> +// pluginManager.getService(name) +// } ?: throw FileNotFoundException("Invalid plugin ID in URI: $uri") +// return plugin.openForRead(uri) + } + } + throw FileNotFoundException("URI not supported by ResourceApi: $uri") + } + + /** + * Opens a stream to the given URI. + * + * @return Never returns null. + * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before + * being passed into this function. + * @throws IOException If the URI cannot be opened. + */ + @JvmOverloads + @Throws(IOException::class) + fun openOutputStream(uri: Uri, append: Boolean = false): OutputStream? { + assertBackgroundThread() + when (getUriType(uri)) { + URI_TYPE_FILE -> { + val localFile = File(uri.path!!) + val parent = localFile.parentFile + if (parent != null) { + parent.mkdirs() + } + return FileOutputStream(localFile, append) + } + + URI_TYPE_CONTENT, URI_TYPE_RESOURCE -> { + val assetFd = + contentResolver.openAssetFileDescriptor(uri, if (append) "wa" else "w") + return assetFd!!.createOutputStream() + } + } + throw FileNotFoundException("URI not supported by ResourceApi: $uri") + } + + @Throws(IOException::class) + fun createHttpConnection(uri: Uri): HttpURLConnection? { + assertBackgroundThread() + return URL(uri.toString()).openConnection() as HttpURLConnection? + } + + // Copies the input to the output in the most efficient manner possible. + // Closes both streams. + @Throws(IOException::class) + fun copyResource(input: OpenForReadResult, outputStream: OutputStream?) { + assertBackgroundThread() + try { + val inputStream = input.inputStream + if (inputStream is FileInputStream && outputStream is FileOutputStream) { + val inChannel = input.inputStream.channel + val outChannel = outputStream.channel + var offset: Long = 0 + val length = input.length + if (input.assetFd != null) { + offset = input.assetFd.startOffset + } + // transferFrom()'s 2nd arg is a relative position. Need to set the absolute + // position first. + inChannel.position(offset) + outChannel.transferFrom(inChannel, 0, length) + } else { + val BUFFER_SIZE = 8192 + val buffer = ByteArray(BUFFER_SIZE) + + while (true) { + val bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE) + + if (bytesRead <= 0) { + break + } + outputStream!!.write(buffer, 0, bytesRead) + } + } + } finally { + input.inputStream.close() + outputStream?.close() + } + } + + @Throws(IOException::class) + fun copyResource(sourceUri: Uri, outputStream: OutputStream?) { + copyResource(openForRead(sourceUri)!!, outputStream) + } + + // Added in 3.5.0. + @Throws(IOException::class) + fun copyResource(sourceUri: Uri, dstUri: Uri) { + copyResource(openForRead(sourceUri)!!, openOutputStream(dstUri)) + } + + private fun assertBackgroundThread() { + if (this.isThreadCheckingEnabled) { + val curThread = Thread.currentThread() + check( + curThread !== Looper.getMainLooper().thread + ) { "Do not perform IO operations on the UI thread. Use Host.getThreadPool() instead." } + check(curThread !== jsThread) { "Tried to perform an IO operation on the WebCore thread. Use Host.getThreadPool() instead." } + } + } + + private fun getDataUriMimeType(uri: Uri): String? { + val uriAsString = uri.schemeSpecificPart + val commaPos = uriAsString.indexOf(',') + if (commaPos == -1) { + return null + } + val mimeParts: Array = + uriAsString.substring(0, commaPos).split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (mimeParts.isNotEmpty()) { + return mimeParts[0] + } + return null + } + + private fun readDataUri(uri: Uri): OpenForReadResult? { + val uriAsString = uri.schemeSpecificPart + val commaPos = uriAsString.indexOf(',') + if (commaPos == -1) { + return null + } + val mimeParts: Array = + uriAsString.substring(0, commaPos).split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + var contentType: String? = null + var base64 = false + if (mimeParts.isNotEmpty()) { + contentType = mimeParts[0] + } + for (i in 1..("_data") + + var jsThread: Thread? = null + + fun getUriType(uri: Uri): Int { + assertNonRelative(uri) + val scheme = uri.scheme + if (ContentResolver.SCHEME_CONTENT.equals(scheme, ignoreCase = true)) { + return URI_TYPE_CONTENT + } + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme, ignoreCase = true)) { + return URI_TYPE_RESOURCE + } + if (ContentResolver.SCHEME_FILE.equals(scheme, ignoreCase = true)) { + if (uri.path!!.startsWith("/android_asset/")) { + return URI_TYPE_ASSET + } + return URI_TYPE_FILE + } + if ("data".equals(scheme, ignoreCase = true)) { + return URI_TYPE_DATA + } + if ("http".equals(scheme, ignoreCase = true)) { + return URI_TYPE_HTTP + } + if ("https".equals(scheme, ignoreCase = true)) { + return URI_TYPE_HTTPS + } + if (PLUGIN_URI_SCHEME.equals(scheme, ignoreCase = true)) { + return URI_TYPE_PLUGIN + } + return URI_TYPE_UNKNOWN + } + + private fun assertNonRelative(uri: Uri) { + require(uri.isAbsolute) { "Relative URIs are not supported." } + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/RuntimeBaseApplication.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/RuntimeBaseApplication.kt new file mode 100644 index 0000000..e0506e3 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/RuntimeBaseApplication.kt @@ -0,0 +1,38 @@ +package com.foxdebug.acode.runtime + +import android.app.Application +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +open class RuntimeBaseApplication : Application() { + private var executorService: ExecutorService? = null + private var executorServiceSingle: ExecutorService? = null + + override fun onCreate() { + super.onCreate() + instance = this + executorService = Executors.newCachedThreadPool() + executorServiceSingle = Executors.newSingleThreadExecutor() + } + + @Deprecated(message = "Directly using System threads or threadPools is not reccomended", replaceWith = ReplaceWith("Coroutines")) + val threadPool: ExecutorService? + get() = executorService + + @Deprecated(message = "Directly using System threads or threadPools is not reccomended", replaceWith = ReplaceWith("Coroutines")) + val threadPoolSingle: ExecutorService? + get() = executorServiceSingle + + override fun onTerminate() { + executorService!!.shutdownNow() + executorServiceSingle!!.shutdownNow() + super.onTerminate() + } + + companion object { + @JvmStatic + var instance: RuntimeBaseApplication? = null + private set + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Service.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Service.kt new file mode 100644 index 0000000..8b9f46a --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/Service.kt @@ -0,0 +1,82 @@ +package com.foxdebug.acode.runtime + +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import com.foxdebug.acode.runtime.ResourceApi.OpenForReadResult +import com.foxdebug.acode.runtime.webview.AppWebView +import org.json.JSONArray +import org.json.JSONException +import java.io.FileNotFoundException +import java.io.IOException + +abstract class Service { + @JvmField + var host: Host? = null + + @JvmField + var webView: AppWebView? = null + + @JvmField + var preferences: Preferences? = null + + open fun initialize(bridgeContext: BridgeContext) { + val webActivity = bridgeContext.webActivity + host = webActivity.getHost() + webView = bridgeContext.webView + preferences = host?.preferences + } + + open fun serviceInitialize() {} + + @Throws(JSONException::class) + open fun execute(action: String, args: String, callback: Callback): Boolean { + return execute(action, JSONArray(args), callback) + } + + @Throws(JSONException::class) + open fun execute(action: String, args: JSONArray, callback: Callback): Boolean { + return false + } + + @Throws(JSONException::class) + open fun onRequestPermissionResult( + requestCode: Int, + permissions: Array?, + grants: IntArray? + ) { + } + + open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + + open fun onMessage(id: String?, data: Any?): Any? { + return null + } + + open fun onNewIntent(intent: Intent?) {} + + open fun onPause(multitasking: Boolean) {} + + open fun onResume(multitasking: Boolean) {} + + open fun onReset() {} + + open fun onConfigurationChanged( + configuration: Configuration? + ) { + } + + open fun onDestroy() {} + + open fun remapUri(uri: Uri?): Uri? { + return null + } + + open val pathHandler: ServicePathHandler? + get() = null + + @Throws(IOException::class) + fun openForRead(uri: Uri): OpenForReadResult? { + throw FileNotFoundException(uri.toString()) + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceDefinition.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceDefinition.kt new file mode 100644 index 0000000..fa8c7b3 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceDefinition.kt @@ -0,0 +1,18 @@ +package com.foxdebug.acode.runtime + +/** Creates one instance of the service bound to a [ServiceName]. */ +fun interface ServiceFactory { + fun create(): Service +} + +/** Binds a transport name to the class serving it, so no lookup happens at runtime. */ +class ServiceDefinition( + val name: ServiceName, + val loadOnStart: Boolean = false, + val factory: ServiceFactory, +) + +/** Services a build variant contributes on top of the shared catalogue. */ +interface ServiceModule { + val services: List +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceName.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceName.kt new file mode 100644 index 0000000..0c76708 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceName.kt @@ -0,0 +1,29 @@ +package com.foxdebug.acode.runtime + +/** Transport keys the JavaScript bridge uses to address native services. */ +enum class ServiceName(val key: String) { + AD_MOB("AdMob"), + AUTHENTICATOR("Authenticator"), + BROWSER("Browser"), + CLIPBOARD("Clipboard"), + NATIVE_HTTP("NativeHttpPlugin"), + BUILD_INFO("BuildInfo"), + CRASH_HANDLER("CrashHandler"), + CUSTOM_TABS("CustomTabs"), + DEVICE("Device"), + FILE("File"), + FTP("Ftp"), + IAP("Iap"), + TEE("Tee"), + SD_CARD("SDcard"), + SERVER("Server"), + SFTP("Sftp"), + SYSTEM("System"), + EXECUTOR("Executor"), + BACKGROUND_EXECUTOR("BackgroundExecutor"), + WEB_SOCKET("WebSocketPlugin"), + ACODE_WEB_VIEW("AcodeWebView"), + APP("App"), + SYSTEM_BAR("SystemBarPlugin"); + +} diff --git a/platforms/android/app/src/main/java/runner/ServicePathHandler.java b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePathHandler.kt similarity index 70% rename from platforms/android/app/src/main/java/runner/ServicePathHandler.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePathHandler.kt index 09b0025..ccc28db 100644 --- a/platforms/android/app/src/main/java/runner/ServicePathHandler.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePathHandler.kt @@ -16,23 +16,11 @@ Licensed to the Apache Software Foundation (ASF) under one specific language governing permissions and limitations under the License. */ +package com.foxdebug.acode.runtime -package runner; - -import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.WebViewAssetLoader.PathHandler /** * Wrapper class for path and handler */ -public class ServicePathHandler { - - private final WebViewAssetLoader.PathHandler handler; - - public ServicePathHandler(WebViewAssetLoader.PathHandler handler) { - this.handler = handler; - } - - public WebViewAssetLoader.PathHandler getPathHandler() { - return handler; - } -} +class ServicePathHandler(val pathHandler: PathHandler?) diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePlugin.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePlugin.kt new file mode 100644 index 0000000..01ee4c2 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServicePlugin.kt @@ -0,0 +1,4 @@ +package com.foxdebug.acode.runtime + +abstract class ServicePlugin { +} \ No newline at end of file diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceRegistry.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceRegistry.kt new file mode 100644 index 0000000..2e1e97b --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/ServiceRegistry.kt @@ -0,0 +1,50 @@ +package com.foxdebug.acode.runtime + +import com.foxdebug.acode.auth.Authenticator +import com.foxdebug.acode.browser.Plugin +import com.foxdebug.acode.buildinfo.BuildInfo +import com.foxdebug.acode.crashhandler.CrashHandler +import com.foxdebug.acode.customtabs.CustomTabsPlugin +import com.foxdebug.acode.device.Device +import com.foxdebug.acode.file.FileUtils +import com.foxdebug.acode.ftp.Ftp +import com.foxdebug.acode.http.NativeHttpPlugin +import com.foxdebug.acode.sdcard.SDcard +import com.foxdebug.acode.security.Tee +import com.foxdebug.acode.server.Server +import com.foxdebug.acode.sftp.Sftp +import com.foxdebug.acode.system.System as SystemService +import com.foxdebug.acode.terminal.BackgroundExecutor +import com.foxdebug.acode.terminal.Executor +import com.foxdebug.acode.websocket.WebSocketPlugin +import com.foxdebug.acode.webview.WebViewPlugin +import com.verso.clipboard.Clipboard + +object ServiceRegistry { + private val mainServices: List = listOf( + ServiceDefinition(ServiceName.AUTHENTICATOR, loadOnStart = true) { Authenticator() }, + ServiceDefinition(ServiceName.BROWSER) { Plugin() }, + ServiceDefinition(ServiceName.CLIPBOARD) { Clipboard() }, + ServiceDefinition(ServiceName.NATIVE_HTTP) { NativeHttpPlugin() }, + ServiceDefinition(ServiceName.BUILD_INFO) { BuildInfo() }, + ServiceDefinition(ServiceName.CRASH_HANDLER, loadOnStart = true) { CrashHandler() }, + ServiceDefinition(ServiceName.CUSTOM_TABS) { CustomTabsPlugin() }, + ServiceDefinition(ServiceName.DEVICE) { Device() }, + ServiceDefinition(ServiceName.FILE, loadOnStart = true) { FileUtils() }, + ServiceDefinition(ServiceName.FTP) { Ftp() }, + ServiceDefinition(ServiceName.TEE) { Tee() }, + ServiceDefinition(ServiceName.SD_CARD) { SDcard() }, + ServiceDefinition(ServiceName.SERVER) { Server() }, + ServiceDefinition(ServiceName.SFTP) { Sftp() }, + ServiceDefinition(ServiceName.SYSTEM) { SystemService() }, + ServiceDefinition(ServiceName.EXECUTOR) { Executor() }, + ServiceDefinition(ServiceName.BACKGROUND_EXECUTOR) { BackgroundExecutor() }, + ServiceDefinition(ServiceName.WEB_SOCKET) { WebSocketPlugin() }, + ServiceDefinition(ServiceName.ACODE_WEB_VIEW) { WebViewPlugin() }, + ServiceDefinition(ServiceName.APP) { App() }, + ServiceDefinition(ServiceName.SYSTEM_BAR, loadOnStart = true) { SystemBarPlugin() }, + ) + + val definitions: List = + mainServices + EditionServices.services + ChannelServices.services +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/SystemBarPlugin.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/SystemBarPlugin.kt new file mode 100644 index 0000000..a5c7c11 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/SystemBarPlugin.kt @@ -0,0 +1,393 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ +package com.foxdebug.acode.runtime + +import android.R +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.os.Build +import android.util.Log +import android.view.View +import android.view.WindowInsetsController +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import com.foxdebug.acode.runtime.webview.AppWebView +import org.json.JSONArray +import org.json.JSONException +import java.util.Objects +import kotlin.math.roundToInt +import androidx.core.view.size +import androidx.core.graphics.toColorInt +import com.foxdebug.acode.settings.AppPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.Dispatcher + +class SystemBarPlugin : Service() { + // Internal variables + private var context: Context? = null + private var resources: Resources? = null + private var overrideStatusBarBackgroundColor: Int? = null + + private var canEdgeToEdge = false + + private lateinit var bridgeContext: BridgeContext + + override fun initialize(bridgeContext: BridgeContext) { + super.initialize(bridgeContext) + this.bridgeContext = bridgeContext + context = bridgeContext.webActivity + resources = bridgeContext.webActivity.resources + canEdgeToEdge = AppPreferences.getBoolean("AndroidEdgeToEdge", false) + } + + @OptIn(DelicateCoroutinesApi::class) + override fun execute(action: String, args: JSONArray, callback: Callback): Boolean { + when (action) { + "setStatusBarVisible" -> { + val visible = args.getBoolean(0) + GlobalScope.launch(Dispatchers.Main) { setStatusBarVisible(visible) } + } + "setStatusBarBackgroundColor" -> { + GlobalScope.launch(Dispatchers.Main) { setStatusBarBackgroundColor(args!!)} + } + else -> { + return false + } + } + + callback.success() + return true + } + + + public override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + GlobalScope.launch(Dispatchers.Main) { + updateSystemBars() + } + } + + public override fun onResume(multitasking: Boolean) { + super.onResume(multitasking) + GlobalScope.launch(Dispatchers.Main) { + updateSystemBars() + } + + } + + override fun onMessage(id: String?, data: Any?): Any? { + if (id == "updateSystemBars") { + GlobalScope.launch(Dispatchers.Main) { + updateSystemBars() + } + } + return null + } + + /** + * Allow the app to override the status bar visibility from JS API. + * If for some reason the statusBarView could not be discovered, it will silently ignore + * the change request + * + * @param visible should the status bar be visible? + */ + private fun setStatusBarVisible(visible: Boolean) { + val statusBar = getStatusBarView(bridgeContext.webView) + if (statusBar != null) { + statusBar.visibility = if (visible) View.VISIBLE else View.GONE + + val rootLayout = getRootLayout(bridgeContext.webView) + if (rootLayout != null) { + ViewCompat.requestApplyInsets(rootLayout) + } + } + } + + /** + * Allow the app to override the status bar background color from JS API. + * If the supplied ARGB is invalid or fails to parse, it will silently ignore + * the change request. + * + * @param argbVals {R, G, B, A} + */ + private fun setStatusBarBackgroundColor(argbVals: JSONArray) { + try { + val r = argbVals.getInt(0) + val g = argbVals.getInt(1) + val b = argbVals.getInt(2) + val a = (255 * argbVals.optDouble(3, 1.0).toFloat()).roundToInt() + + overrideStatusBarBackgroundColor = Color.argb(a, r, g, b) + updateStatusBar(overrideStatusBarBackgroundColor!!) + } catch (e: JSONException) { + // Silently skip + } + } + + /** + * Attempt to update all system bars (status, navigation and gesture bars) in various points + * of the apps life cycle. + * For example: + * 1. Device configurations between (E.g. between dark and light mode) + * 2. User resumes the app + * 3. App transitions from SplashScreen Theme to App's Theme + */ + private fun updateSystemBars() { + // Update Root View Background Color + var rootViewBackgroundColor = this.preferenceBackgroundColor + if (rootViewBackgroundColor == null) { + rootViewBackgroundColor = if (canEdgeToEdge) Color.TRANSPARENT else this.uiModeColor + } + updateRootView(rootViewBackgroundColor) + + // Update StatusBar Background Color + val statusBarBackgroundColor: Int? + if (overrideStatusBarBackgroundColor != null) { + statusBarBackgroundColor = overrideStatusBarBackgroundColor + } else if (AppPreferences.contains("StatusBarBackgroundColor")) { + statusBarBackgroundColor = this.preferenceStatusBarBackgroundColor + } else if (AppPreferences.contains("BackgroundColor")) { + statusBarBackgroundColor = rootViewBackgroundColor + } else { + statusBarBackgroundColor = if (canEdgeToEdge) Color.TRANSPARENT else this.uiModeColor + } + + updateStatusBar(statusBarBackgroundColor!!) + } + + /** + * Updates the root layout's background color with the supplied color int. + * It will also determine if the background color is light or dark to properly adjust the + * appearance of the navigation/gesture bar's icons so it will not clash with the background. + * + * + * System bars (navigation & gesture) on SDK 25 or lower is forced to black as the appearance + * of the fonts can not be updated. + * System bars (navigation & gesture) on SDK 26 or greater allows custom background color. + * + * + * + * @param bgColor Background color + */ + @Suppress("deprecation") + private fun updateRootView(bgColor: Int) { + val window = bridgeContext.webActivity.getWindow() + + // Set the root view's background color. Works on SDK 36+ + val root = bridgeContext.webActivity.findViewById(R.id.content) + if (root != null) root.setBackgroundColor(bgColor) + + // Automatically set the font and icon color of the system bars based on background color. + val isBackgroundColorLight: Boolean + if (bgColor == Color.TRANSPARENT) { + isBackgroundColorLight = isColorLight(this.uiModeColor) + } else { + isBackgroundColorLight = isColorLight(bgColor) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val controller = window.insetsController + if (controller != null) { + val appearance = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + if (isBackgroundColorLight) { + controller.setSystemBarsAppearance(0, appearance) + } else { + controller.setSystemBarsAppearance(appearance, appearance) + } + } + } + val controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()) + controllerCompat.isAppearanceLightNavigationBars = isBackgroundColorLight + + window.navigationBarColor = bgColor + } + + /** + * Updates the statusBarView background color with the supplied color int. + * It will also determine if the background color is light or dark to properly adjust the + * appearance of the status bar so the font will not clash with the background. + * + * @param bgColor Background color + */ + private fun updateStatusBar(bgColor: Int) { + val window = bridgeContext.webActivity.window + + val statusBar = getStatusBarView(bridgeContext.webView) + statusBar?.setBackgroundColor(bgColor) + + // Automatically set the font and icon color of the system bars based on background color. + val isStatusBarBackgroundColorLight: Boolean + if (bgColor == Color.TRANSPARENT) { + isStatusBarBackgroundColorLight = isColorLight(this.uiModeColor) + } else { + isStatusBarBackgroundColorLight = isColorLight(bgColor) + } + val controllerCompat = WindowCompat.getInsetsController(window, window.decorView) + controllerCompat.isAppearanceLightStatusBars = isStatusBarBackgroundColorLight + } + + private val preferenceStatusBarBackgroundColor: Int + /** + * Returns the StatusBarBackgroundColor preference value or [.getUiModeColor] as fallback. + * + * @return Integer + */ + get() { + val colorString = + AppPreferences.getString("StatusBarBackgroundColor", null) + return Objects.requireNonNullElse( + parseColorFromString(colorString), + this.uiModeColor + ) + } + + private val preferenceBackgroundColor: Int? + /** + * Returns the BackgroundColor preference value. + * If the value is missing or fails to decode, null is returned. + * + * @return Integer|null + */ + get() { + if (!AppPreferences.contains("BackgroundColor")) { + return null + } + + try { + return AppPreferences.getInteger("BackgroundColor", 0) + } catch (e: NumberFormatException) { + Log.e( + PLUGIN_NAME, + "Invalid background color argument. Example valid string: '0x00000000'" + ) + return null + } + } + + /** + * Tries to find and return the rootLayout. + * + * @param webView AppView + * @return FrameLayout|null + */ + private fun getRootLayout(webView: AppWebView): FrameLayout? { + val parent = webView.view.parent + if (parent is FrameLayout) { + return parent + } + + return null + } + + /** + * Tries to find and return the statusBarView. + * + * @param webView AppView + * @return View|null + */ + private fun getStatusBarView(webView: AppWebView): View? { + val rootView = getRootLayout(webView) ?: return null + + for (i in 0.. 0.5 + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeChromeClient.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeChromeClient.kt new file mode 100644 index 0000000..6d28bf7 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeChromeClient.kt @@ -0,0 +1,198 @@ +package com.foxdebug.acode.runtime.webview + +import android.Manifest +import android.graphics.Color +import android.net.Uri +import android.util.Log +import android.view.View +import android.webkit.ConsoleMessage +import android.webkit.GeolocationPermissions +import android.webkit.JsPromptResult +import android.webkit.JsResult +import android.webkit.PermissionRequest +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.widget.FrameLayout +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import com.foxdebug.acode.runtime.BaseWebActivity + +class AcodeChromeClient( + private val activity: BaseWebActivity, + private val webView: AppWebView, +) : WebChromeClient() { + private val dialogs = Dialogs(activity) + private val fileChooser = FileChooser(activity.getHost()) + private var pendingPermission: PermissionRequest? = null + private var fullscreenCallback: WebChromeClient.CustomViewCallback? = null + private var fullscreenView: FrameLayout? = null + private var backHandler = false + + private val permissionLauncher: ActivityResultLauncher> = + activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { grants -> + val request = pendingPermission + pendingPermission = null + if (request == null) return@registerForActivityResult + if (grants.isNotEmpty() && !grants.containsValue(false)) { + request.grant(request.resources) + } else { + request.deny() + } + } + + private val immersive = ImmersiveFullscreen(activity, true) { backHandler = false } + + val isFullscreen: Boolean + get() = fullscreenView != null + + fun pause() { + immersive.pause() + } + + fun resume() { + immersive.resume() + } + + fun setBackHandler(enabled: Boolean) { + if (enabled && !isFullscreen) { + throw IllegalStateException("Fullscreen is not active.") + } + backHandler = enabled + } + + fun setOrientation(orientation: String?) { + if (orientation == null) immersive.unlockOrientation() + else immersive.lockOrientation(orientation) + } + + fun handleBack(): Boolean { + if (!isFullscreen) return false + if (backHandler) webView.fireDocumentEvent("fullscreenbackbutton") + else onHideCustomView() + return true + } + + fun reset() { + onHideCustomView() + dialogs.destroyLastDialog() + } + + override fun onShowCustomView( + view: View, + callback: CustomViewCallback, + ) { + if (isFullscreen) { + callback.onCustomViewHidden() + return + } + val container = FrameLayout(activity) + container.setBackgroundColor(Color.BLACK) + container.addView(view, FrameLayout.LayoutParams(-1, -1)) + fullscreenCallback = callback + fullscreenView = container + activity.contentView.addView(container, FrameLayout.LayoutParams(-1, -1)) + webView.visibility = View.INVISIBLE + immersive.enter(container) + ViewCompat.requestApplyInsets(activity.contentView) + } + + override fun onHideCustomView() { + val container = fullscreenView ?: return + immersive.exit() + activity.contentView.removeView(container) + fullscreenView = null + webView.visibility = View.VISIBLE + val callback = fullscreenCallback + fullscreenCallback = null + callback?.onCustomViewHidden() + ViewCompat.requestApplyInsets(activity.contentView) + } + + override fun onShowFileChooser( + view: WebView?, + filePathCallback: ValueCallback?>?, + fileChooserParams: WebChromeClient.FileChooserParams?, + ): Boolean = fileChooser.show(view!!, filePathCallback!!, fileChooserParams!!) + + override fun onJsAlert( + view: WebView?, + url: String?, + message: String?, + result: JsResult, + ): Boolean { + dialogs.showAlert(message) { success, _ -> + if (success) result.confirm() else result.cancel() + } + return true + } + + override fun onJsConfirm( + view: WebView?, + url: String?, + message: String?, + result: JsResult, + ): Boolean { + dialogs.showConfirm(message) { success, _ -> + if (success) result.confirm() else result.cancel() + } + return true + } + + override fun onJsPrompt( + view: WebView?, + url: String?, + message: String?, + defaultValue: String?, + result: JsPromptResult, + ): Boolean { + dialogs.showPrompt(message, defaultValue) { success, input -> + if (success) result.confirm(input) else result.cancel() + } + return true + } + + override fun onGeolocationPermissionsShowPrompt( + origin: String?, + callback: GeolocationPermissions.Callback, + ) { + callback.invoke(origin, true, false) + } + + override fun onPermissionRequest(request: PermissionRequest) { + val permissions = mutableListOf() + for (resource in request.resources) { + when (resource) { + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> + permissions.add(Manifest.permission.CAMERA) + + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + permissions.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissions.add(Manifest.permission.RECORD_AUDIO) + } + } + } + if (permissions.isEmpty()) { + request.grant(request.resources) + } else { + pendingPermission?.deny() + pendingPermission = request + permissionLauncher.launch(permissions.toTypedArray()) + } + } + + override fun onPermissionRequestCanceled(request: PermissionRequest?) { + if (pendingPermission === request) pendingPermission = null + } + + override fun onConsoleMessage(message: ConsoleMessage): Boolean { + Log.d( + "AcodeWebView", + "${message.message()} at ${message.sourceId()}:${message.lineNumber()}", + ) + return true + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeWebViewClient.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeWebViewClient.kt new file mode 100644 index 0000000..462383e --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AcodeWebViewClient.kt @@ -0,0 +1,118 @@ +package com.foxdebug.acode.runtime.webview + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.graphics.Bitmap +import android.net.http.SslError +import android.util.Log +import android.webkit.RenderProcessGoneDetail +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import com.foxdebug.acode.BuildConfig +import com.foxdebug.acode.runtime.BaseWebActivity +import com.foxdebug.acode.runtime.Bridge +import com.foxdebug.acode.runtime.ServiceName +import java.io.ByteArrayInputStream +import java.io.IOException +import java.net.URLConnection +import java.util.Arrays + +class AcodeWebViewClient(private val activity: BaseWebActivity, private val bridge: Bridge) : + WebViewClient() { + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler, + error: SslError? + ) { + if (BuildConfig.DEBUG) handler.proceed() + else handler.cancel() + } + + override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean { + Log.e("AcodeWebViewClient","RenderProcessGone details: $detail") + return super.onRenderProcessGone(view, detail) + } + + override fun onPageStarted( + view: WebView, + url: String?, + icon: Bitmap? + ) { + (view as AppWebView).resetChrome() + bridge.reset() + super.onPageStarted(view, url, icon) + } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest + ): Boolean { + val uri = request.getUrl() + if ("https" == uri.getScheme() || "http" == uri.getScheme() + ) return false + if (!request.isForMainFrame()) return false + try { + activity.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } catch (ignored: ActivityNotFoundException) { + } + return true + } + + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest + ): WebResourceResponse? { + val uri = request.getUrl() + if ("https" != uri.getScheme() || "localhost" != uri.getHost() + ) return null + var path = uri.getPath() + if (path == null || path == "/") path = "/index.html" + val file = bridge.getService(ServiceName.FILE) + val handler = file!!.pathHandler + val response = handler!! + .pathHandler!! + .handle(path.substring(1)) + if (response != null) return response + if (listOf(*path.split("/".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()).contains("..") || + path.indexOf('\u0000') != -1 + ) return missing() + try { + val stream = activity.getAssets().open("bundle" + path) + var mime = if (path.endsWith(".js")) + "application/javascript" + else + if (path.endsWith(".wasm")) + "application/wasm" + else + URLConnection.guessContentTypeFromName(path) + if (mime == null) mime = "application/octet-stream" + return WebResourceResponse( + mime, + if (mime.startsWith("text/") || mime == "application/javascript") + "UTF-8" + else + null, + stream + ) + } catch (exception: IOException) { + return missing() + } + } + + companion object { + private fun missing(): WebResourceResponse { + return WebResourceResponse( + "text/plain", + "UTF-8", + 404, + "Not Found", + mutableMapOf(), + ByteArrayInputStream(ByteArray(0)) + ) + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AppWebView.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AppWebView.kt new file mode 100644 index 0000000..734c3be --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/AppWebView.kt @@ -0,0 +1,154 @@ +package com.foxdebug.acode.runtime.webview + +import android.content.Context +import android.graphics.Rect +import android.text.InputType +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import android.webkit.WebView +import com.foxdebug.acode.runtime.Bridge +import com.foxdebug.acode.runtime.ResourceApi +import org.json.JSONObject + +class AppWebView(context: Context) : WebView(context) { + private var inputType = -1 + private var nativeContextMenuDisabled = false + var bridge: Bridge? = null + private set + var resourceApi: ResourceApi? = null + private set + private var fullscreen: AcodeChromeClient? = null + + fun initialize(bridge: Bridge) { + this.bridge = bridge + resourceApi = ResourceApi(getContext(), bridge) + } + + val view: AppWebView + get() = this + + val engine: AppWebView + get() = this + + fun setInputType(type: Int) { + inputType = type + } + + fun setNativeContextMenuDisabled(disabled: Boolean) { + nativeContextMenuDisabled = disabled + } + + fun setFullscreenController(fullscreen: AcodeChromeClient) { + this.fullscreen = fullscreen + } + + fun resetChrome() { + fullscreen!!.reset() + } + + fun setFullscreenBackHandler(enabled: Boolean) { + fullscreen!!.setBackHandler(enabled) + } + + fun setFullscreenOrientation(orientation: String?) { + fullscreen!!.setOrientation(orientation) + } + + fun fireDocumentEvent(event: String?) { + evaluateJavascript( + "window.Bridge && Bridge.fireDocumentEvent(" + + JSONObject.quote(event) + + ");", + null + ) + } + + override fun onCreateInputConnection(attrs: EditorInfo): InputConnection? { + val connection = super.onCreateInputConnection(attrs) + if (inputType == 0) attrs.inputType = attrs.inputType or + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + else if (inputType == 1) attrs.inputType = + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + return connection + } + + override fun startActionMode(callback: ActionMode.Callback?): ActionMode? { + return suppress(super.startActionMode(wrap(callback))) + } + + override fun startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? { + return suppress(super.startActionMode(wrap(callback), type)) + } + + override fun startActionModeForChild( + child: View?, + callback: ActionMode.Callback? + ): ActionMode? { + return suppress(super.startActionModeForChild(child, wrap(callback))) + } + + override fun startActionModeForChild( + child: View?, + callback: ActionMode.Callback?, + type: Int + ): ActionMode? { + return suppress(super.startActionModeForChild(child, wrap(callback), type)) + } + + private fun wrap(callback: ActionMode.Callback?): ActionMode.Callback? { + if (!nativeContextMenuDisabled || callback == null) return callback + return object : ActionMode.Callback2() { + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val created = callback.onCreateActionMode(mode, menu) + if (created) suppressUi(mode, menu) + return created + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val prepared = callback.onPrepareActionMode(mode, menu) + suppressUi(mode, menu) + return prepared + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return callback.onActionItemClicked(mode, item) + } + + override fun onDestroyActionMode(mode: ActionMode?) { + callback.onDestroyActionMode(mode) + } + + override fun onGetContentRect(mode: ActionMode?, view: View?, rect: Rect?) { + if (callback is ActionMode.Callback2) callback.onGetContentRect(mode, view, rect) + else super.onGetContentRect(mode, view, rect) + } + } + } + + private fun suppress(mode: ActionMode?): ActionMode? { + if (nativeContextMenuDisabled && mode != null) suppressUi( + mode, + mode.menu + ) + return mode + } + + private fun suppressUi(mode: ActionMode?, menu: Menu?) { + if (mode == null || !nativeContextMenuDisabled || menu == null) return + menu.clear() + mode.title = null + mode.subtitle = null + post { + if (!nativeContextMenuDisabled) return@post + try { + mode.hide(0) + } catch (ignored: Throwable) { + } + } + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/Dialogs.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/Dialogs.kt new file mode 100644 index 0000000..622c517 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/Dialogs.kt @@ -0,0 +1,120 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ +package com.foxdebug.acode.runtime.webview + +import android.R +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.view.KeyEvent +import android.widget.EditText + +/** + * Helper class for WebViews to implement prompt(), alert(), confirm() dialogs. + */ +class Dialogs(private val context: Context?) { + private var lastHandledDialog: AlertDialog? = null + + fun showAlert(message: String?, result: Result) { + val dlg = AlertDialog.Builder(context) + dlg.setMessage(message) + dlg.setTitle("Alert") + //Don't let alerts break the back button + dlg.setCancelable(true) + dlg.setPositiveButton( + R.string.ok + ) { dialog, which -> result.gotResult(true, null) } + dlg.setOnCancelListener { result.gotResult(false, null) } + dlg.setOnKeyListener(object : DialogInterface.OnKeyListener { + //DO NOTHING + override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + result.gotResult(true, null) + return false + } else return true + } + }) + lastHandledDialog = dlg.show() + } + + fun showConfirm(message: String?, result: Result) { + val dlg = AlertDialog.Builder(context) + dlg.setMessage(message) + dlg.setTitle("Confirm") + dlg.setCancelable(true) + dlg.setPositiveButton( + R.string.ok + ) { dialog, which -> result.gotResult(true, null) } + dlg.setNegativeButton( + R.string.cancel + ) { dialog, which -> result.gotResult(false, null) } + dlg.setOnCancelListener { result.gotResult(false, null) } + dlg.setOnKeyListener(object : DialogInterface.OnKeyListener { + //DO NOTHING + override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + result.gotResult(false, null) + return false + } else return true + } + }) + lastHandledDialog = dlg.show() + } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * + * Since we are hacking prompts for our own purposes, we should not be using them for + * this purpose, perhaps we should hack console.log to do this instead! + */ + fun showPrompt(message: String?, defaultValue: String?, result: Result) { + // Returning false would also show a dialog, but the default one shows the origin (ugly). + val dlg = AlertDialog.Builder(context) + dlg.setMessage(message) + val input = EditText(context) + if (defaultValue != null) { + input.setText(defaultValue) + } + dlg.setView(input) + dlg.setCancelable(false) + dlg.setPositiveButton( + R.string.ok + ) { dialog, which -> + val userText = input.text.toString() + result.gotResult(true, userText) + } + dlg.setNegativeButton( + R.string.cancel + ) { dialog, which -> result.gotResult(false, null) } + lastHandledDialog = dlg.show() + } + + fun destroyLastDialog() { + if (lastHandledDialog != null) { + lastHandledDialog!!.cancel() + } + } + + fun interface Result { + fun gotResult(success: Boolean, value: String?) + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/FileChooser.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/FileChooser.kt new file mode 100644 index 0000000..f8b275b --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/FileChooser.kt @@ -0,0 +1,179 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +*/ +package com.foxdebug.acode.runtime.webview + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import android.webkit.ValueCallback +import android.webkit.WebChromeClient.FileChooserParams +import android.webkit.WebView +import androidx.core.content.FileProvider +import com.foxdebug.acode.runtime.Host +import com.foxdebug.acode.runtime.Service +import java.io.File +import java.io.IOException + +internal class FileChooser(private val host: Host) { + fun show( + webView: WebView, + filePathsCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ): Boolean { + val fileIntent = fileChooserParams.createIntent() + + // Check if multiple-select is specified + var selectMultiple = false + if (fileChooserParams.getMode() == + FileChooserParams.MODE_OPEN_MULTIPLE + ) { + selectMultiple = true + } + fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple) + + // Uses Intent.EXTRA_MIME_TYPES to pass multiple mime types. + val acceptTypes = fileChooserParams.getAcceptTypes() + if (acceptTypes.size > 1) { + fileIntent.setType("*/*") // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES. + fileIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes) + } + + // Image from camera intent + var tempUri: Uri? = null + var captureIntent: Intent? = null + if (fileChooserParams.isCaptureEnabled()) { + captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + val context = webView.getContext() + if (context + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) && + captureIntent.resolveActivity(context.getPackageManager()) != null + ) { + try { + val tempFile = createTempFile(context) + Log.d(LOG_TAG, "Temporary photo capture file: " + tempFile) + tempUri = createUriForFile(context, tempFile) + Log.d(LOG_TAG, "Temporary photo capture URI: " + tempUri) + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri) + } catch (e: IOException) { + Log.e( + LOG_TAG, + "Unable to create temporary file for photo capture", + e + ) + captureIntent = null + } + } else { + Log.w(LOG_TAG, "Device does not support photo capture") + captureIntent = null + } + } + val captureUri = tempUri + + // Chooser intent + val chooserIntent = Intent.createChooser(fileIntent, null) + if (captureIntent != null) { + chooserIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, arrayOf( + captureIntent, + ) + ) + } + + try { + Log.i(LOG_TAG, "Starting intent for file chooser") + host.startActivityForResult( + object : Service() { + public override fun onActivityResult( + requestCode: Int, + resultCode: Int, + intent: Intent? + ) { + // Handle result + var result: Array? = null + if (resultCode == Activity.RESULT_OK) { + val uris: MutableList = ArrayList() + + if (intent != null && intent.getData() != null) { + // single file + Log.v( + LOG_TAG, + "Adding file (single): " + intent.getData() + ) + uris.add(intent.getData()) + } else if (captureUri != null) { + // camera + Log.v(LOG_TAG, "Adding camera capture: " + captureUri) + uris.add(captureUri) + } else if (intent != null && intent.getClipData() != null) { + // multiple files + val clipData = intent.getClipData() + val count = clipData!!.getItemCount() + for (i in 0..() + } + } + filePathsCallback.onReceiveValue(result) + } + }, + chooserIntent, + 0 + ) + } catch (e: ActivityNotFoundException) { + Log.w( + LOG_TAG, + "No activity found to handle file chooser intent.", + e + ) + filePathsCallback.onReceiveValue(null) + } + return true + } + + @Throws(IOException::class) + private fun createTempFile(context: Context): File { + // Create an image file name + return File.createTempFile("temp", ".jpg", context.getCacheDir()) + } + + @Throws(IOException::class) + private fun createUriForFile(context: Context, tempFile: File): Uri? { + val appId = context.getPackageName() + return FileProvider.getUriForFile(context, appId + ".provider", tempFile) + } + + companion object { + private const val LOG_TAG = "AcodeFileChooser" + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/ImmersiveFullscreen.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/ImmersiveFullscreen.kt new file mode 100644 index 0000000..b3e1e04 --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/runtime/webview/ImmersiveFullscreen.kt @@ -0,0 +1,208 @@ +package com.foxdebug.acode.runtime.webview + +import android.app.Activity +import android.content.pm.ActivityInfo +import android.os.Build +import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.OnWindowFocusChangeListener +import android.view.Window +import android.view.WindowManager +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +/** Owns window state only while Acode is displaying a browser fullscreen view. */ +internal class ImmersiveFullscreen + (private val activity: Activity, private var resumed: Boolean, private val onExit: Runnable) : + OnWindowFocusChangeListener, OnAttachStateChangeListener { + private val window: Window + private val decor: View + private val controller: WindowInsetsControllerCompat + private var fullscreenView: View? = null + private var focusObserver: ViewTreeObserver? = null + private var statusBarVisible = false + private var navigationBarVisible = false + private var previousBehavior = 0 + private var previousCutoutMode = 0 + private var previousLegacyFlags = 0 + private var requestedOrientation: Int? = null + private var previousOrientation = 0 + private var orientationApplied = false + + init { + window = activity.getWindow() + decor = window.getDecorView() + controller = WindowCompat.getInsetsController(window, decor) + } + + @Suppress("deprecation") + fun enter(view: View) { + if (fullscreenView != null) return + + val insets = ViewCompat.getRootWindowInsets(decor) + val flags = decor.getSystemUiVisibility() + statusBarVisible = + if (insets != null) + insets.isVisible(WindowInsetsCompat.Type.statusBars()) + else + (flags and View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 && + (window.getAttributes().flags and + WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0 + navigationBarVisible = + if (insets != null) + insets.isVisible(WindowInsetsCompat.Type.navigationBars()) + else + (flags and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0 + previousBehavior = controller.getSystemBarsBehavior() + previousLegacyFlags = flags and LEGACY_FULLSCREEN_FLAGS + if (Build.VERSION.SDK_INT >= 28) { + val attributes = window.getAttributes() + previousCutoutMode = attributes.layoutInDisplayCutoutMode + attributes.layoutInDisplayCutoutMode = + if (Build.VERSION.SDK_INT >= 30) + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + else + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + window.setAttributes(attributes) + } + + fullscreenView = view + // The wrapper fills Acode's root, with no status/navigation bar margins. + // Only the keyboard should shrink the usable area. Leave the root's inset + // listener alone so normal editor layout is unchanged on exit. + ViewCompat.setOnApplyWindowInsetsListener( + view, + OnApplyWindowInsetsListener { target: View?, appliedInsets: WindowInsetsCompat? -> + val bottom = appliedInsets!! + .getInsets(WindowInsetsCompat.Type.ime()) + .bottom + target!!.setPadding(0, 0, 0, bottom) + appliedInsets + }) + view.addOnAttachStateChangeListener(this) + focusObserver = decor.getViewTreeObserver() + focusObserver!!.addOnWindowFocusChangeListener(this) + reapply() + ViewCompat.requestApplyInsets(view) + } + + fun reapply() { + if (!resumed || fullscreenView == null || !fullscreenView!!.isAttachedToWindow() || !decor.hasWindowFocus() + ) return + controller.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + ) + controller.hide(WindowInsetsCompat.Type.systemBars()) + } + + fun lockOrientation(orientation: String?) { + val requested: Int + if ("landscape" == orientation) { + requested = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } else if ("portrait" == orientation) { + requested = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + } else { + throw IllegalArgumentException( + "Orientation must be landscape or portrait." + ) + } + check( + !(!resumed || fullscreenView == null || !fullscreenView!!.isAttachedToWindow() + ) + ) { "Orientation requires foreground fullscreen." } + + val previous = activity.getRequestedOrientation() + activity.setRequestedOrientation(requested) + // Only a successful first request owns the restoration state. Changing + // modes or resuming this session must not replace it with our own override. + if (requestedOrientation == null) previousOrientation = previous + requestedOrientation = requested + orientationApplied = true + } + + fun unlockOrientation() { + restoreOrientation() + requestedOrientation = null + } + + fun pause() { + resumed = false + restoreOrientation() + } + + fun resume() { + resumed = true + if (fullscreenView != null && + fullscreenView!!.isAttachedToWindow() && requestedOrientation != null + ) { + activity.setRequestedOrientation(requestedOrientation!!) + orientationApplied = true + } + reapply() + } + + private fun restoreOrientation() { + if (!orientationApplied) return + activity.setRequestedOrientation(previousOrientation) + orientationApplied = false + } + + @Suppress("deprecation") + fun exit() { + if (fullscreenView == null) return + val view = fullscreenView + fullscreenView = null + onExit.run() + unlockOrientation() + view!!.removeOnAttachStateChangeListener(this) + ViewCompat.setOnApplyWindowInsetsListener(view, null) + view.setPadding(0, 0, 0, 0) + if (focusObserver!!.isAlive() + ) focusObserver!!.removeOnWindowFocusChangeListener(this) + focusObserver = null + + controller.setSystemBarsBehavior(previousBehavior) + restoreBar(WindowInsetsCompat.Type.statusBars(), statusBarVisible) + restoreBar(WindowInsetsCompat.Type.navigationBars(), navigationBarVisible) + if (Build.VERSION.SDK_INT < 30) { + // Restore only the bits we own; keep any theme changes made meanwhile. + decor.setSystemUiVisibility( + (decor.getSystemUiVisibility() and LEGACY_FULLSCREEN_FLAGS.inv()) or + previousLegacyFlags + ) + } + if (Build.VERSION.SDK_INT >= 28) { + val attributes = window.getAttributes() + attributes.layoutInDisplayCutoutMode = previousCutoutMode + window.setAttributes(attributes) + } + ViewCompat.requestApplyInsets(decor) + } + + private fun restoreBar(type: Int, visible: Boolean) { + if (visible) controller.show(type) + else controller.hide(type) + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + if (hasFocus) reapply() + } + + override fun onViewAttachedToWindow(view: View) {} + + override fun onViewDetachedFromWindow(view: View) { + exit() + } + + companion object { + @Deprecated("Using old methods, needs rewrite") + private const val LEGACY_FULLSCREEN_FLAGS = View.SYSTEM_UI_FLAG_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_IMMERSIVE or + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + } +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/sdcard/SDcard.java b/platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/SDcard.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/sdcard/SDcard.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/SDcard.java index a945578..5e902b4 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/sdcard/SDcard.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/SDcard.java @@ -1,4 +1,4 @@ -package com.foxdebug.sdcard; +package com.foxdebug.acode.sdcard; import android.app.Activity; import android.content.ContentResolver; @@ -6,7 +6,6 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Environment; import android.os.FileObserver; import android.os.storage.StorageManager; @@ -32,14 +31,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; -import java.util.Map; + import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; -import runner.Payload; + +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -63,11 +62,11 @@ public class SDcard extends Service { private HashMap fileObservers = new HashMap(); private WorkspaceIndex workspaceIndex; - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.REQUEST_CODE = this.ACCESS_INTENT; - this.context = host.getContext(); - this.activity = host.getActivity(); + this.context = bridgeContext.getWebActivity(); + this.activity = bridgeContext.getWebActivity(); this.contentResolver = this.context.getContentResolver(); this.workspaceIndex = new WorkspaceIndex(this.context); this.storageManager = (StorageManager) this.activity.getSystemService( diff --git a/platforms/android/app/src/main/java/com/foxdebug/sdcard/WorkspaceIndex.java b/platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/WorkspaceIndex.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/sdcard/WorkspaceIndex.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/WorkspaceIndex.java index f69e3fc..fca431d 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/sdcard/WorkspaceIndex.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/WorkspaceIndex.java @@ -1,4 +1,4 @@ -package com.foxdebug.sdcard; +package com.foxdebug.acode.sdcard; import android.content.ContentResolver; import android.content.ContentValues; @@ -33,8 +33,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import runner.Callback; -import runner.Payload; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/plugin/Tee.java b/platforms/android/app/src/main/java/com/foxdebug/acode/security/Tee.java similarity index 96% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/plugin/Tee.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/security/Tee.java index 1b7963a..445509a 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/plugin/Tee.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/security/Tee.java @@ -1,7 +1,7 @@ -package com.foxdebug.acode.rk.plugin; +package com.foxdebug.acode.security; -import runner.Callback; -import runner.Service; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -14,11 +14,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import android.content.Context; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; //auth plugin -import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; +import com.foxdebug.acode.auth.EncryptedPreferenceManager; public class Tee extends Service { @@ -39,8 +38,8 @@ public class Tee extends Service { private Context context; - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.context = host.getContext(); } diff --git a/platforms/android/app/src/main/java/com/foxdebug/server/NanoHTTPDWebserver.java b/platforms/android/app/src/main/java/com/foxdebug/acode/server/NanoHTTPDWebserver.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/server/NanoHTTPDWebserver.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/server/NanoHTTPDWebserver.java index 198d65a..da9e469 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/server/NanoHTTPDWebserver.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/server/NanoHTTPDWebserver.java @@ -1,4 +1,4 @@ -package com.foxdebug.server; +package com.foxdebug.acode.server; import android.content.ContentResolver; import android.content.Context; @@ -16,8 +16,8 @@ import java.util.Iterator; import java.util.Map; import java.util.UUID; -import runner.Callback; -import runner.Payload; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/com/foxdebug/server/Server.java b/platforms/android/app/src/main/java/com/foxdebug/acode/server/Server.java similarity index 95% rename from platforms/android/app/src/main/java/com/foxdebug/server/Server.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/server/Server.java index 9f13b77..3a15b77 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/server/Server.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/server/Server.java @@ -1,9 +1,8 @@ -package com.foxdebug.server; +package com.foxdebug.acode.server; import java.io.IOException; import java.util.HashMap; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; import org.json.JSONArray; import org.json.JSONException; @@ -12,8 +11,8 @@ public class Server extends Service { public HashMap servers; @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); servers = new HashMap(); } diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/settings/AppPreferences.kt b/platforms/android/app/src/main/java/com/foxdebug/acode/settings/AppPreferences.kt new file mode 100644 index 0000000..8e2e5aa --- /dev/null +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/settings/AppPreferences.kt @@ -0,0 +1,80 @@ +package com.foxdebug.acode.settings + +import android.content.Context +import android.content.SharedPreferences +import com.foxdebug.acode.runtime.RuntimeBaseApplication +import androidx.core.content.edit + +object AppPreferences { + //do not change this name + private const val PREFERENCES_NAME = "Settings" + + private val preferences: SharedPreferences by lazy { + val application = RuntimeBaseApplication.instance + ?: error("AppPreferences requires RuntimeBaseApplication to be initialized") + application.applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + } + + fun set(name: String, value: String?) { + preferences.edit { putString(name, value) } + } + + fun set(name: String, value: Boolean) { + preferences.edit { putBoolean(name, value) } + } + + fun set(name: String, value: Int) { + preferences.edit { putInt(name, value) } + } + + fun set(name: String, value: Long) { + preferences.edit { putLong(name, value) } + } + + fun set(name: String, value: Float) { + preferences.edit { putFloat(name, value) } + } + + fun set(name: String, value: Set) { + preferences.edit { putStringSet(name, value) } + } + + fun getString(name: String, defaultValue: String? = null): String? = + preferences.getString(name, defaultValue) + + fun getBoolean(name: String, defaultValue: Boolean = false): Boolean = + preferences.getBoolean(name, defaultValue) + + fun getInteger(name: String, defaultValue: Int = 0): Int = + preferences.getInt(name, defaultValue) + + fun getLong(name: String, defaultValue: Long = 0L): Long = + preferences.getLong(name, defaultValue) + + fun getFloat(name: String, defaultValue: Float = 0f): Float = + preferences.getFloat(name, defaultValue) + + fun getStringSet(name: String, defaultValue: Set = emptySet()): Set = + preferences.getStringSet(name, defaultValue) ?: defaultValue + + fun contains(name: String): Boolean = preferences.contains(name) + + fun remove(name: String) { + preferences.edit { remove(name) } + } + + fun clear() { + preferences.edit { clear() } + } + + fun registerChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + preferences.registerOnSharedPreferenceChangeListener(listener) + } + + fun unregisterChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + preferences.unregisterOnSharedPreferenceChangeListener(listener) + } + + val all: Map + get() = preferences.all +} diff --git a/platforms/android/app/src/main/java/com/foxdebug/sftp/Sftp.java b/platforms/android/app/src/main/java/com/foxdebug/acode/sftp/Sftp.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/sftp/Sftp.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/sftp/Sftp.java index 10293e6..f1822e1 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/sftp/Sftp.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/sftp/Sftp.java @@ -1,4 +1,4 @@ -package com.foxdebug.sftp; +package com.foxdebug.acode.sftp; import android.app.Activity; import android.app.AlertDialog; @@ -44,7 +44,6 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.ByteBuffer; -import java.nio.channels.UnresolvedAddressException; import java.nio.charset.StandardCharsets; import java.security.Security; import java.util.Map; @@ -57,11 +56,10 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; -import runner.Payload; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -319,8 +317,8 @@ public void run() { } } - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); context = host.getContext(); activity = host.getActivity(); securityStore = new SftpSecurityStore(context); diff --git a/platforms/android/app/src/main/java/com/foxdebug/sftp/SftpSecurityStore.java b/platforms/android/app/src/main/java/com/foxdebug/acode/sftp/SftpSecurityStore.java similarity index 96% rename from platforms/android/app/src/main/java/com/foxdebug/sftp/SftpSecurityStore.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/sftp/SftpSecurityStore.java index 9951566..425eca5 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/sftp/SftpSecurityStore.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/sftp/SftpSecurityStore.java @@ -1,7 +1,7 @@ -package com.foxdebug.sftp; +package com.foxdebug.acode.sftp; import android.content.Context; -import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; +import com.foxdebug.acode.auth.EncryptedPreferenceManager; import java.security.GeneralSecurityException; import java.util.UUID; import org.json.JSONException; diff --git a/platforms/android/app/src/main/java/com/foxdebug/system/RewardPassManager.java b/platforms/android/app/src/main/java/com/foxdebug/acode/system/RewardPassManager.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/system/RewardPassManager.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/system/RewardPassManager.java index d947cf7..bbd4b1a 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/system/RewardPassManager.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/system/RewardPassManager.java @@ -1,8 +1,8 @@ -package com.foxdebug.system; +package com.foxdebug.acode.system; import android.content.Context; import android.util.Log; -import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; +import com.foxdebug.acode.auth.EncryptedPreferenceManager; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; diff --git a/platforms/android/app/src/main/java/com/foxdebug/system/StreamHttp.java b/platforms/android/app/src/main/java/com/foxdebug/acode/system/StreamHttp.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/system/StreamHttp.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/system/StreamHttp.java index a57d8a9..2eeb494 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/system/StreamHttp.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/system/StreamHttp.java @@ -1,4 +1,4 @@ -package com.foxdebug.system; +package com.foxdebug.acode.system; import android.util.Base64; import android.util.Log; @@ -14,8 +14,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import runner.Callback; -import runner.Payload; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/com/foxdebug/system/System.java b/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/system/System.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java index 4cc0669..a35ccee 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/system/System.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java @@ -1,4 +1,4 @@ -package com.foxdebug.system; +package com.foxdebug.acode.system; import static android.os.Build.VERSION.SDK_INT; @@ -39,7 +39,8 @@ import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.documentfile.provider.DocumentFile; -import com.foxdebug.system.Ui.Theme; +import com.foxdebug.acode.BuildConfig; +import com.foxdebug.acode.system.Ui.Theme; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -62,12 +63,12 @@ import java.nio.file.StandardOpenOption; import java.security.MessageDigest; import java.util.*; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; -import runner.webView.AppView; -import runner.Payload; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.webview.AppWebView; +import com.foxdebug.acode.runtime.Payload; +import com.foxdebug.acode.settings.AppPreferences; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -107,15 +108,13 @@ public class System extends Service { private int systemBarColor = 0xFF000000; private Theme theme; private Callback intentHandler; - private AppView webView; private String fileProviderAuthority; private RewardPassManager rewardPassManager; - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.context = host.getContext(); this.activity = host.getActivity(); - this.webView = webView; this.rewardPassManager = new RewardPassManager(this.context); this.activity.runOnUiThread( new Runnable() { @@ -185,7 +184,7 @@ public boolean execute( final boolean fullscreenBackHandler = args.getBoolean(0); activity.runOnUiThread(() -> { try { - ((AppView) webView).setFullscreenBackHandler(fullscreenBackHandler); + ((AppWebView) webView).setFullscreenBackHandler(fullscreenBackHandler); callbackContext.success(); } catch (RuntimeException error) { callbackContext.error(error.getMessage()); @@ -196,7 +195,7 @@ public boolean execute( final String orientation = args.isNull(0) ? null : args.getString(0); activity.runOnUiThread(() -> { try { - ((AppView) webView).setFullscreenOrientation(orientation); + ((AppWebView) webView).setFullscreenOrientation(orientation); callbackContext.success(); } catch (RuntimeException error) { callbackContext.error(error.getMessage()); @@ -247,8 +246,7 @@ public void run() { shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, text); - host - .getActivity() + host.getActivity() .startActivity(Intent.createChooser(shareIntent, "Share")); callbackContext.success(); @@ -1833,7 +1831,7 @@ private void setUiTheme( this.systemBarColor = Color.parseColor(systemBarColor); this.theme = new Theme(scheme); - preferences.set("BackgroundColor", this.systemBarColor); + AppPreferences.INSTANCE.set("BackgroundColor", this.systemBarColor); webView.getBridge().postMessage("updateSystemBars", null); applySystemBarTheme(); @@ -2363,6 +2361,8 @@ private void setNativeContextMenuDisabled(boolean disabled) { private void setAppIcon(String iconName, Callback callback) { try { String packageName = context.getPackageName(); + // Alias components sit in the namespace package, not the application id. + String componentPackage = BuildConfig.class.getPackage().getName(); PackageManager pm = context.getPackageManager(); String key = iconName == null ? "default" : iconName.toLowerCase(); @@ -2373,7 +2373,7 @@ private void setAppIcon(String iconName, Callback callback) { } pm.setComponentEnabledSetting( - new ComponentName(packageName, packageName + "." + targetAlias), + new ComponentName(packageName, componentPackage + "." + targetAlias), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ); @@ -2383,7 +2383,7 @@ private void setAppIcon(String iconName, Callback callback) { continue; } pm.setComponentEnabledSetting( - new ComponentName(packageName, packageName + "." + entry.getValue()), + new ComponentName(packageName, componentPackage + "." + entry.getValue()), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ); diff --git a/platforms/android/app/src/main/java/com/foxdebug/system/Ui.java b/platforms/android/app/src/main/java/com/foxdebug/acode/system/Ui.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/system/Ui.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/system/Ui.java index 4cdf19c..e478ad3 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/system/Ui.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/system/Ui.java @@ -1,4 +1,4 @@ -package com.foxdebug.system; +package com.foxdebug.acode.system; import android.content.Context; import android.graphics.Bitmap; diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/AlpineDocumentProvider.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/AlpineDocumentProvider.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/AlpineDocumentProvider.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/AlpineDocumentProvider.java index 97a4dd5..193891e 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/AlpineDocumentProvider.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/AlpineDocumentProvider.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import android.content.ComponentName; import android.content.Context; @@ -20,7 +20,6 @@ import java.util.LinkedList; import java.util.Locale; import com.foxdebug.acode.R; -import com.foxdebug.acode.rk.exec.terminal.*; public class AlpineDocumentProvider extends DocumentsProvider { diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/BackgroundExecutor.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/BackgroundExecutor.java similarity index 97% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/BackgroundExecutor.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/BackgroundExecutor.java index c7fb992..ae5c243 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/BackgroundExecutor.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/BackgroundExecutor.java @@ -1,12 +1,10 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; import org.json.*; import java.io.*; import java.util.*; import java.util.concurrent.*; -import com.foxdebug.acode.rk.exec.terminal.*; public class BackgroundExecutor extends Service { @@ -17,8 +15,8 @@ public class BackgroundExecutor extends Service { private ProcessManager processManager; @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.processManager = new ProcessManager(host.getContext()); } diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/Executor.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/Executor.java similarity index 96% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/Executor.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/Executor.java index 5af1a98..19e0f46 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/Executor.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/Executor.java @@ -1,7 +1,6 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; import org.json.*; import android.content.ComponentName; @@ -23,12 +22,9 @@ import android.content.pm.PackageManager; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.app.Activity; -import com.foxdebug.acode.rk.exec.terminal.*; import java.net.ServerSocket; @@ -72,8 +68,8 @@ private void askNotificationPermission(Activity context) { } @Override - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); this.context = host.getContext(); this.activity = host.getActivity(); askNotificationPermission(activity); @@ -189,7 +185,7 @@ public void handleMessage(Message msg) { String data = bundle.getString("data"); if (action.equals("exec_result")) { - runner.Callback callbackContext = getCallbackContext(id); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(id); if (callbackContext != null) { if (bundle.getBoolean("isSuccess", false)) { callbackContext.success(data); @@ -200,7 +196,7 @@ public void handleMessage(Message msg) { } } else { String pid = id; - runner.Callback callbackContext = getCallbackContext(pid); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(pid); if (callbackContext != null) { switch (action) { @@ -396,7 +392,7 @@ private void stopServiceNow() { } private void startProcess(String pid, String cmd, String alpine) { - runner.Callback callbackContext = getCallbackContext(pid); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(pid); if (callbackContext != null) { Payload result = new Payload(Payload.Status.OK, pid); result.setKeepCallback(true); @@ -432,7 +428,7 @@ private void exec(String execId, String cmd, String alpine) { try { serviceMessenger.send(msg); } catch (RemoteException e) { - runner.Callback callbackContext = getCallbackContext(execId); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(execId); if (callbackContext != null) { callbackContext.error("Failed to execute command: " + e.getMessage()); cleanupCallback(execId); @@ -476,7 +472,7 @@ private void isProcessRunning(String pid) { try { serviceMessenger.send(msg); } catch (RemoteException e) { - runner.Callback callbackContext = getCallbackContext(pid); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(pid); if (callbackContext != null) { callbackContext.error("Check running error: " + e.getMessage()); cleanupCallback(pid); @@ -493,7 +489,7 @@ private void listProcesses(String requestId) { try { serviceMessenger.send(msg); } catch (RemoteException e) { - runner.Callback callbackContext = getCallbackContext(requestId); + com.foxdebug.acode.runtime.Callback callbackContext = getCallbackContext(requestId); if (callbackContext != null) { callbackContext.error("List processes error: " + e.getMessage()); cleanupCallback(requestId); diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessManager.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessManager.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessManager.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessManager.java index 51c94d2..458c3b2 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessManager.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessManager.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import android.content.Context; import android.content.pm.PackageManager; @@ -9,7 +9,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import com.foxdebug.acode.rk.exec.terminal.*; public class ProcessManager { diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessServer.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessServer.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessServer.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessServer.java index c6c2641..ad6b078 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessServer.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessServer.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessUtils.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessUtils.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessUtils.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessUtils.java index 003084f..21d0cec 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/ProcessUtils.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/ProcessUtils.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import java.lang.reflect.Field; import java.io.*; @@ -6,7 +6,6 @@ import org.json.JSONArray; import org.json.JSONObject; import android.util.Log; -import com.foxdebug.acode.rk.exec.terminal.*; public class ProcessUtils { diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/StreamHandler.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/StreamHandler.java similarity index 90% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/StreamHandler.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/StreamHandler.java index ab4c643..07afdac 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/StreamHandler.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/StreamHandler.java @@ -1,7 +1,6 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import java.io.*; -import com.foxdebug.acode.rk.exec.terminal.*; public class StreamHandler { public interface OutputListener { diff --git a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/TerminalService.java b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/TerminalService.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/TerminalService.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/terminal/TerminalService.java index f662664..b081574 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/TerminalService.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/terminal/TerminalService.java @@ -1,4 +1,4 @@ -package com.foxdebug.acode.rk.exec.terminal; +package com.foxdebug.acode.terminal; import android.app.Notification; import android.app.NotificationChannel; @@ -24,7 +24,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; -import com.foxdebug.acode.rk.exec.terminal.*; public class TerminalService extends Service { diff --git a/platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketInstance.java b/platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketInstance.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketInstance.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketInstance.java index 281c4ee..9dbd377 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketInstance.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketInstance.java @@ -1,13 +1,13 @@ -package com.foxdebug.websocket; +package com.foxdebug.acode.websocket; import android.util.Base64; import android.util.Log; import androidx.annotation.NonNull; -import runner.*; -import runner.Callback; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; +import com.foxdebug.acode.runtime.Callback; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; diff --git a/platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketPlugin.java b/platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketPlugin.java similarity index 97% rename from platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketPlugin.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketPlugin.java index 336d2a4..8c087ec 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/websocket/WebSocketPlugin.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/websocket/WebSocketPlugin.java @@ -1,9 +1,9 @@ -package com.foxdebug.websocket; +package com.foxdebug.acode.websocket; import android.util.Log; -import runner.*; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.*; + import org.json.*; import java.util.UUID; @@ -17,7 +17,7 @@ public class WebSocketPlugin extends Service { public OkHttpClient okHttpMainClient = null; @Override - protected void serviceInitialize() { + public void serviceInitialize() { this.okHttpMainClient = new OkHttpClient(); } diff --git a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewActivity.java b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewActivity.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/webview/WebViewActivity.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewActivity.java index eea46c2..86ac08c 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewActivity.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewActivity.java @@ -1,4 +1,4 @@ -package com.foxdebug.webview; +package com.foxdebug.acode.webview; import android.app.Activity; import android.os.Build; diff --git a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewInstance.java b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewInstance.java similarity index 99% rename from platforms/android/app/src/main/java/com/foxdebug/webview/WebViewInstance.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewInstance.java index 86ccad2..3ede45b 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewInstance.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewInstance.java @@ -1,4 +1,4 @@ -package com.foxdebug.webview; +package com.foxdebug.acode.webview; import android.app.Activity; import android.app.AlertDialog; @@ -27,7 +27,7 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -import runner.Callback; +import com.foxdebug.acode.runtime.Callback; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; diff --git a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewPlugin.java b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewPlugin.java similarity index 98% rename from platforms/android/app/src/main/java/com/foxdebug/webview/WebViewPlugin.java rename to platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewPlugin.java index 0f819d0..c69410f 100644 --- a/platforms/android/app/src/main/java/com/foxdebug/webview/WebViewPlugin.java +++ b/platforms/android/app/src/main/java/com/foxdebug/acode/webview/WebViewPlugin.java @@ -1,4 +1,4 @@ -package com.foxdebug.webview; +package com.foxdebug.acode.webview; import android.app.DownloadManager; import android.content.Context; @@ -11,9 +11,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.UUID; -import runner.Callback; -import runner.Service; -import runner.Payload; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -27,7 +27,7 @@ public class WebViewPlugin extends Service { private Callback messageCallback; @Override - protected void serviceInitialize() { + public void serviceInitialize() { instance = this; } diff --git a/platforms/android/app/src/main/java/com/verso/clipboard/Clipboard.java b/platforms/android/app/src/main/java/com/verso/clipboard/Clipboard.java index 72c2ffb..c152421 100644 --- a/platforms/android/app/src/main/java/com/verso/clipboard/Clipboard.java +++ b/platforms/android/app/src/main/java/com/verso/clipboard/Clipboard.java @@ -1,8 +1,8 @@ package com.verso.clipboard; -import runner.Service; -import runner.Payload; -import runner.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.Payload; +import com.foxdebug.acode.runtime.Callback; import org.json.JSONArray; import org.json.JSONException; diff --git a/platforms/android/app/src/main/java/runner/AcodeApplication.java b/platforms/android/app/src/main/java/runner/AcodeApplication.java deleted file mode 100644 index 8e0e7b8..0000000 --- a/platforms/android/app/src/main/java/runner/AcodeApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package runner; - -import android.app.Application; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class AcodeApplication extends Application { - private static AcodeApplication instance; - - private ExecutorService executorService; - private ExecutorService executorServiceSingle; - - public static AcodeApplication getInstance() { - return instance; - } - - @Override - public void onCreate() { - super.onCreate(); - instance = this; - executorService = Executors.newCachedThreadPool(); - executorServiceSingle = Executors.newSingleThreadExecutor(); - } - - public ExecutorService getThreadPool() { - return executorService; - } - - public ExecutorService getThreadPoolSingle() { - return executorServiceSingle; - } - - @Override - public void onTerminate() { - executorService.shutdownNow(); - executorServiceSingle.shutdownNow(); - super.onTerminate(); - } -} diff --git a/platforms/android/app/src/main/java/runner/App.java b/platforms/android/app/src/main/java/runner/App.java deleted file mode 100644 index 5457843..0000000 --- a/platforms/android/app/src/main/java/runner/App.java +++ /dev/null @@ -1,35 +0,0 @@ -package runner; - -import org.json.JSONArray; -import org.json.JSONException; - -public final class App extends Service { - - @Override - public boolean execute(String action, JSONArray args, Callback callback) - throws JSONException { - switch (action) { - case "exitApp": - host.getActivity().runOnUiThread(() -> host.getActivity().finish()); - break; - case "overrideButton": - host - .getActivity() - .overrideButton(args.getString(0), args.getBoolean(1)); - break; - case "clearCache": - webView.post(() -> webView.clearCache(true)); - break; - case "clearHistory": - webView.post(() -> webView.clearHistory()); - break; - case "backHistory": - webView.post(() -> webView.goBack()); - break; - default: - return false; - } - callback.success(); - return true; - } -} diff --git a/platforms/android/app/src/main/java/runner/Bridge.java b/platforms/android/app/src/main/java/runner/Bridge.java deleted file mode 100644 index a1c729d..0000000 --- a/platforms/android/app/src/main/java/runner/Bridge.java +++ /dev/null @@ -1,147 +0,0 @@ -package runner; - -import android.net.Uri; -import android.webkit.JavascriptInterface; -import com.foxdebug.acode.BuildConfig; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.Map; -import org.json.JSONArray; -import org.json.JSONObject; -import runner.webView.AppView; - -public final class Bridge { - - private final Host host; - private final AppView webView; - private final Map registry = new LinkedHashMap<>(); - private final Map services = new LinkedHashMap<>(); - private volatile long generation; - - public Bridge(Host host, AppView webView) { - this.host = host; - this.webView = webView; - try ( - java.io.InputStream input = host - .getContext() - .getAssets() - .open("services.json") - ) { - java.io.ByteArrayOutputStream output = - new java.io.ByteArrayOutputStream(); - byte[] buffer = new byte[4096]; - int count; - while ((count = input.read(buffer)) != -1) output.write(buffer, 0, count); - JSONArray definitions = new JSONArray( - output.toString(StandardCharsets.UTF_8.name()) - ); - for (int index = 0; index < definitions.length(); index++) { - JSONObject definition = definitions.getJSONObject(index); - String variant = definition.getString("variant"); - if ( - variant.equals("free") && !BuildConfig.FLAVOR.equals("free") - ) continue; - if (variant.equals("store") && BuildConfig.FDROID) continue; - registry.put(definition.getString("name"), definition); - } - } catch (Exception exception) { - throw new IllegalStateException( - "Unable to load native services", - exception - ); - } - } - - public void initialize() { - for (JSONObject definition : registry.values()) { - if (definition.optBoolean("onload")) getService( - definition.optString("name") - ); - } - } - - public synchronized Service getService(String name) { - if (services.containsKey(name)) return services.get(name); - JSONObject definition = registry.get(name); - if (definition == null) return null; - try { - Service service = (Service) Class.forName(definition.getString("class")) - .getDeclaredConstructor() - .newInstance(); - service.initialize(host, webView); - service.serviceInitialize(); - services.put(name, service); - return service; - } catch (Exception exception) { - throw new IllegalStateException( - "Unable to initialize " + name, - exception - ); - } - } - - public synchronized ArrayList getServices() { - return new ArrayList<>(services.values()); - } - - public long getGeneration() { - return generation; - } - - @JavascriptInterface - public boolean exec(String name, String action, String args, long id) { - Callback callback = new Callback(id, webView); - long requestGeneration = generation; - webView.post(() -> { - if (generation != requestGeneration) return; - try { - Service service = getService(name); - if (service == null) { - callback.error("Unknown native service: " + name); - return; - } - AcodeApplication.getInstance() - .getThreadPoolSingle() - .execute(() -> { - if (generation != requestGeneration) return; - try { - if (!service.execute(action, args, callback)) { - callback.sendPayload( - new Payload(Payload.Status.INVALID_ACTION, action) - ); - } - } catch (Exception exception) { - callback.error(exception.toString()); - } - }); - } catch (Exception exception) { - callback.error(exception.toString()); - } - }); - return true; - } - - public Uri remapUri(Uri uri) { - for (Service service : getServices()) { - Uri result = service.remapUri(uri); - if (result != null) return result; - } - return null; - } - - public void postMessage(String name, Object value) { - for (Service service : getServices()) service.onMessage(name, value); - } - - public void reset() { - generation++; - for (Service service : getServices()) service.onReset(); - } - - public void destroy() { - generation++; - for (Service service : getServices()) service.onDestroy(); - services.clear(); - } -} diff --git a/platforms/android/app/src/main/java/runner/Callback.java b/platforms/android/app/src/main/java/runner/Callback.java deleted file mode 100644 index a1743ca..0000000 --- a/platforms/android/app/src/main/java/runner/Callback.java +++ /dev/null @@ -1,61 +0,0 @@ -package runner; - -import android.util.Log; -import org.json.JSONException; -import runner.webView.AppView; - -public class Callback { - - private final long id; - private final AppView webView; - private final long generation; - private boolean finished; - - public Callback(long id, AppView webView) { - this.id = id; - this.webView = webView; - this.generation = webView.getBridge().getGeneration(); - } - - public synchronized boolean isFinished() { - return finished; - } - - public String getCallbackId() { - return Long.toString(id); - } - - public void success() { - sendPayload(new Payload(Payload.Status.OK)); - } - - public void success(Object data) { - sendPayload(new Payload(Payload.Status.OK, data)); - } - - public void error(Object data) { - sendPayload(new Payload(Payload.Status.ERROR, data)); - } - - public synchronized void sendPayload(Payload payload) { - if (finished) return; - finished = !payload.getKeepCallback(); - if ( - payload.getStatus() == Payload.Status.NO_RESULT.ordinal() && - payload.getKeepCallback() - ) return; - try { - String json = payload.toJSON(id, generation).toString(); - webView.post(() -> { - if (generation == webView.getBridge().getGeneration()) { - webView.evaluateJavascript( - "window.Android.callback(" + json + ");", - null - ); - } - }); - } catch (JSONException exception) { - Log.e("Acode", "Unable to serialize callback", exception); - } - } -} diff --git a/platforms/android/app/src/main/java/runner/Host.java b/platforms/android/app/src/main/java/runner/Host.java deleted file mode 100644 index 21065e6..0000000 --- a/platforms/android/app/src/main/java/runner/Host.java +++ /dev/null @@ -1,105 +0,0 @@ -package runner; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import androidx.core.app.ActivityCompat; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import org.json.JSONException; - -public final class Host { - - private final MainActivity activity; - private final Preferences preferences = new Preferences(); - private final Map permissionRequests = new HashMap<>(); - private final Map activityRequests = new HashMap<>(); - private int requestId = 40000; - - public Host(MainActivity activity) { - this.activity = activity; - preferences.set("AndroidPersistentFileLocation", "Compatibility"); - preferences.set("AndroidBlacklistSecureSocketProtocols", "SSLv3,TLSv1"); - preferences.set("scheme", "https"); - preferences.set("hostname", "localhost"); - preferences.set("BackgroundColor", 0xff313131); - } - - public MainActivity getActivity() { - return activity; - } - - public Context getContext() { - return activity; - } - - public Preferences getPreferences() { - return preferences; - } - - public ExecutorService getThreadPool() { - return AcodeApplication.getInstance().getThreadPool(); - } - - public boolean hasPermission(String permission) { - return ( - ActivityCompat.checkSelfPermission(activity, permission) == - PackageManager.PERMISSION_GRANTED - ); - } - - public void requestPermission(Service service, int code, String permission) { - requestPermissions(service, code, new String[] {permission}); - } - - public void requestPermissions( - Service service, - int code, - String[] permissions - ) { - activity.runOnUiThread(() -> { - int id = requestId++; - permissionRequests.put(id, new Request(service, code)); - ActivityCompat.requestPermissions(activity, permissions, id); - }); - } - - public void startActivityForResult(Service service, Intent intent, int code) { - activity.runOnUiThread(() -> { - int id = requestId++; - activityRequests.put(id, new Request(service, code)); - activity.startActivityForResult(intent, id); - }); - } - - public boolean onPermissionsResult(int id, String[] permissions, int[] grants) - throws JSONException { - Request request = permissionRequests.remove(id); - if (request == null) return false; - request.service.onRequestPermissionResult( - request.code, - permissions, - grants - ); - return true; - } - - public boolean onActivityResult(int id, int result, Intent data) { - Request request = activityRequests.remove(id); - if (request == null) return false; - request.service.onActivityResult(request.code, result, data); - return true; - } - - private static final class Request { - - final Service service; - final int code; - - Request(Service service, int code) { - this.service = service; - this.code = code; - } - } -} diff --git a/platforms/android/app/src/main/java/runner/LOG.java b/platforms/android/app/src/main/java/runner/LOG.java deleted file mode 100644 index 916afb8..0000000 --- a/platforms/android/app/src/main/java/runner/LOG.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ -package runner; - -import android.util.Log; - -/** - * Log to Android logging system. - * - *

Log message can be a string or a printf formatted string with arguments. - * See Formatter

- */ -public class LOG { - - public static final int VERBOSE = Log.VERBOSE; - public static final int DEBUG = Log.DEBUG; - public static final int INFO = Log.INFO; - public static final int WARN = Log.WARN; - public static final int ERROR = Log.ERROR; - - // Current log level - public static int LOGLEVEL = Log.ERROR; - - /** - * Set the current log level. - * - * @param logLevel - */ - public static void setLogLevel(int logLevel) { - LOGLEVEL = logLevel; - Log.i("NativeLog", "Changing log level to " + logLevel); - } - - /** - * Set the current log level. - * - * @param logLevel - */ - public static void setLogLevel(String logLevel) { - if ("VERBOSE".equals(logLevel)) LOGLEVEL = VERBOSE; - else if ("DEBUG".equals(logLevel)) LOGLEVEL = DEBUG; - else if ("INFO".equals(logLevel)) LOGLEVEL = INFO; - else if ("WARN".equals(logLevel)) LOGLEVEL = WARN; - else if ("ERROR".equals(logLevel)) LOGLEVEL = ERROR; - Log.i("NativeLog", "Changing log level to " + logLevel + "(" + LOGLEVEL + ")"); - } - - /** - * Determine if log level will be logged - * - * @param logLevel - * @return true if the parameter passed in is greater than or equal to the current log level - */ - public static boolean isLoggable(int logLevel) { - return (logLevel >= LOGLEVEL); - } - - /** - * Verbose log message. - * - * @param tag - * @param s - */ - public static void v(String tag, String s) { - if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s); - } - - /** - * Debug log message. - * - * @param tag - * @param s - */ - public static void d(String tag, String s) { - if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s); - } - - /** - * Info log message. - * - * @param tag - * @param s - */ - public static void i(String tag, String s) { - if (LOG.INFO >= LOGLEVEL) Log.i(tag, s); - } - - /** - * Warning log message. - * - * @param tag - * @param s - */ - public static void w(String tag, String s) { - if (LOG.WARN >= LOGLEVEL) Log.w(tag, s); - } - - /** - * Error log message. - * - * @param tag - * @param s - */ - public static void e(String tag, String s) { - if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s); - } - - /** - * Verbose log message. - * - * @param tag - * @param s - * @param e - */ - public static void v(String tag, String s, Throwable e) { - if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, s, e); - } - - /** - * Debug log message. - * - * @param tag - * @param s - * @param e - */ - public static void d(String tag, String s, Throwable e) { - if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, s, e); - } - - /** - * Info log message. - * - * @param tag - * @param s - * @param e - */ - public static void i(String tag, String s, Throwable e) { - if (LOG.INFO >= LOGLEVEL) Log.i(tag, s, e); - } - - /** - * Warning log message. - * - * @param tag - * @param e - */ - public static void w(String tag, Throwable e) { - if (LOG.WARN >= LOGLEVEL) Log.w(tag, e); - } - - /** - * Warning log message. - * - * @param tag - * @param s - * @param e - */ - public static void w(String tag, String s, Throwable e) { - if (LOG.WARN >= LOGLEVEL) Log.w(tag, s, e); - } - - /** - * Error log message. - * - * @param tag - * @param s - * @param e - */ - public static void e(String tag, String s, Throwable e) { - if (LOG.ERROR >= LOGLEVEL) Log.e(tag, s, e); - } - - /** - * Verbose log message with printf formatting. - * - * @param tag - * @param s - * @param args - */ - public static void v(String tag, String s, Object... args) { - if (LOG.VERBOSE >= LOGLEVEL) Log.v(tag, String.format(s, args)); - } - - /** - * Debug log message with printf formatting. - * - * @param tag - * @param s - * @param args - */ - public static void d(String tag, String s, Object... args) { - if (LOG.DEBUG >= LOGLEVEL) Log.d(tag, String.format(s, args)); - } - - /** - * Info log message with printf formatting. - * - * @param tag - * @param s - * @param args - */ - public static void i(String tag, String s, Object... args) { - if (LOG.INFO >= LOGLEVEL) Log.i(tag, String.format(s, args)); - } - - /** - * Warning log message with printf formatting. - * - * @param tag - * @param s - * @param args - */ - public static void w(String tag, String s, Object... args) { - if (LOG.WARN >= LOGLEVEL) Log.w(tag, String.format(s, args)); - } - - /** - * Error log message with printf formatting. - * - * @param tag - * @param s - * @param args - */ - public static void e(String tag, String s, Object... args) { - if (LOG.ERROR >= LOGLEVEL) Log.e(tag, String.format(s, args)); - } - -} diff --git a/platforms/android/app/src/main/java/runner/MainActivity.java b/platforms/android/app/src/main/java/runner/MainActivity.java deleted file mode 100644 index 89cf3b6..0000000 --- a/platforms/android/app/src/main/java/runner/MainActivity.java +++ /dev/null @@ -1,241 +0,0 @@ -package runner; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.webkit.WebView; -import android.widget.FrameLayout; -import androidx.activity.OnBackPressedCallback; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.splashscreen.SplashScreen; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import com.foxdebug.acode.BuildConfig; -import java.lang.ref.WeakReference; -import runner.webView.AppView; -import runner.webView.ChromeClient; -import runner.webView.WebViewClient; - -public class MainActivity extends AppCompatActivity { - - private static WeakReference context; - private AppView appView; - private Host host; - private Bridge bridge; - private ChromeClient chromeClient; - private FrameLayout content; - private boolean hasPaused; - private final java.util.Set overriddenButtons = - java.util.concurrent.ConcurrentHashMap.newKeySet(); - - public static Context getContext() { - return context == null ? null : context.get(); - } - - @Override - public void onCreate(Bundle state) { - SplashScreen.installSplashScreen(this); - super.onCreate(state); - context = new WeakReference<>(this); - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - host = new Host(this); - content = new FrameLayout(this); - appView = new AppView(this); - bridge = new Bridge(host, appView); - appView.initialize(bridge); - chromeClient = new ChromeClient(this, appView); - appView.setWebViewClient(new WebViewClient(this, bridge)); - appView.setWebChromeClient(chromeClient); - appView.setFullscreenController(chromeClient); - appView.addJavascriptInterface(bridge, "Android"); - appView.setVerticalScrollBarEnabled(false); - appView.getSettings().setJavaScriptEnabled(true); - appView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); - appView.getSettings().setSaveFormData(false); - appView.getSettings().setGeolocationEnabled(true); - android.webkit.CookieManager.getInstance().setAcceptThirdPartyCookies( - appView, - true - ); - appView.getSettings().setDomStorageEnabled(true); - appView.getSettings().setDatabaseEnabled(true); - appView.getSettings().setAllowFileAccess(true); - appView.getSettings().setAllowContentAccess(true); - appView.getSettings().setMediaPlaybackRequiresUserGesture(false); - appView.setOverScrollMode(View.OVER_SCROLL_NEVER); - appView.setBackgroundColor(0xff313131); - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG); - content.addView(appView, new FrameLayout.LayoutParams(-1, -1)); - View statusBar = new View(this); - statusBar.setTag("statusBarView"); - content.addView(statusBar); - setContentView(content); - ViewCompat.setOnApplyWindowInsetsListener(content, (view, insets) -> { - androidx.core.graphics.Insets bars = insets.getInsets( - WindowInsetsCompat.Type.systemBars() | - WindowInsetsCompat.Type.displayCutout() - ); - int keyboard = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; - boolean fullscreen = chromeClient.isFullscreen(); - int top = - !fullscreen && statusBar.getVisibility() != View.GONE ? bars.top : 0; - FrameLayout.LayoutParams params = - (FrameLayout.LayoutParams) appView.getLayoutParams(); - int left = fullscreen ? 0 : bars.left; - int right = fullscreen ? 0 : bars.right; - int bottom = fullscreen ? 0 : Math.max(bars.bottom, keyboard); - if ( - params.leftMargin != left || - params.topMargin != top || - params.rightMargin != right || - params.bottomMargin != bottom - ) { - params.setMargins(left, top, right, bottom); - appView.setLayoutParams(params); - } - statusBar.setLayoutParams( - new FrameLayout.LayoutParams(-1, top, android.view.Gravity.TOP) - ); - return insets; - }); - getOnBackPressedDispatcher().addCallback( - this, - new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - dispatchBack(); - } - } - ); - bridge.initialize(); - appView.loadUrl("https://localhost/index.html"); - } - - public void overrideButton(String button, boolean enabled) { - if (button.equals("volumeup") || button.equals("volumedown")) button += - "button"; - if (enabled) overriddenButtons.add(button); - else overriddenButtons.remove(button); - } - - @Override - public boolean dispatchKeyEvent(android.view.KeyEvent event) { - if (appView == null) return super.dispatchKeyEvent(event); - int code = event.getKeyCode(); - if ( - code == android.view.KeyEvent.KEYCODE_BACK && - (chromeClient.isFullscreen() || overriddenButtons.contains("backbutton")) - ) { - if (event.getAction() == android.view.KeyEvent.ACTION_UP) dispatchBack(); - return true; - } - String name = - code == android.view.KeyEvent.KEYCODE_MENU - ? "menubutton" - : code == android.view.KeyEvent.KEYCODE_VOLUME_UP - ? "volumeupbutton" - : code == android.view.KeyEvent.KEYCODE_VOLUME_DOWN - ? "volumedownbutton" - : null; - if (name != null && overriddenButtons.contains(name)) { - if ( - event.getAction() == android.view.KeyEvent.ACTION_UP - ) appView.fireDocumentEvent(name); - return true; - } - return super.dispatchKeyEvent(event); - } - - private void dispatchBack() { - if (chromeClient.handleBack()) return; - if (overriddenButtons.contains("backbutton")) appView.fireDocumentEvent( - "backbutton" - ); - else if (appView.canGoBack()) appView.goBack(); - else finish(); - } - - public Host getHost() { - return host; - } - - public FrameLayout getContentView() { - return content; - } - - @Override - public void onConfigurationChanged( - android.content.res.Configuration configuration - ) { - super.onConfigurationChanged(configuration); - for (Service service : bridge.getServices()) - service.onConfigurationChanged(configuration); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - for (Service service : bridge.getServices()) service.onNewIntent(intent); - } - - @Override - protected void onPause() { - super.onPause(); - if (appView == null) return; - hasPaused = true; - appView.fireDocumentEvent("pause"); - for (Service service : bridge.getServices()) service.onPause(true); - chromeClient.pause(); - } - - @Override - protected void onResume() { - super.onResume(); - if (appView == null) return; - appView.onResume(); - appView.resumeTimers(); - chromeClient.resume(); - for (Service service : bridge.getServices()) service.onResume(true); - if (hasPaused) appView.fireDocumentEvent("resume"); - } - - @Override - protected void onActivityResult(int code, int result, Intent data) { - super.onActivityResult(code, result, data); - if (!host.onActivityResult(code, result, data)) { - for (Service service : bridge.getServices()) - service.onActivityResult(code, result, data); - } - } - - @Override - public void onRequestPermissionsResult( - int code, - String[] permissions, - int[] grants - ) { - super.onRequestPermissionsResult(code, permissions, grants); - try { - if (!host.onPermissionsResult(code, permissions, grants)) { - for (Service service : bridge.getServices()) - service.onRequestPermissionResult(code, permissions, grants); - } - } catch (org.json.JSONException exception) { - android.util.Log.e("Acode", "Permission callback failed", exception); - } - } - - @Override - protected void onDestroy() { - if (bridge != null) bridge.destroy(); - if (chromeClient != null) chromeClient.reset(); - if (appView != null) { - content.removeView(appView); - appView.destroy(); - } - super.onDestroy(); - } -} diff --git a/platforms/android/app/src/main/java/runner/Payload.java b/platforms/android/app/src/main/java/runner/Payload.java deleted file mode 100644 index 405d05e..0000000 --- a/platforms/android/app/src/main/java/runner/Payload.java +++ /dev/null @@ -1,106 +0,0 @@ -package runner; - -import android.util.Base64; -import java.util.List; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public final class Payload { - - public static final int MESSAGE_TYPE_STRING = 1; - public static final int MESSAGE_TYPE_ARRAYBUFFER = 6; - public static final int MESSAGE_TYPE_BINARYSTRING = 7; - - public enum Status { - NO_RESULT, - OK, - CLASS_NOT_FOUND_EXCEPTION, - ILLEGAL_ACCESS_EXCEPTION, - INSTANTIATION_EXCEPTION, - MALFORMED_URL_EXCEPTION, - IO_EXCEPTION, - INVALID_ACTION, - JSON_EXCEPTION, - ERROR, - } - - private static final String[] STATUS_MESSAGES = { - "No result", - "OK", - "Class not found", - "Illegal access", - "Instantiation error", - "Malformed url", - "IO error", - "Invalid action", - "JSON error", - "Error", - }; - - private final Status status; - private final Object data; - private boolean keepAlive; - - public Payload(Status status) { - this(status, STATUS_MESSAGES[status.ordinal()]); - } - - public Payload(Status status, Object data) { - this.status = status; - this.data = data; - } - - public Payload(Status status, byte[] data, boolean binaryString) { - this(status, encodeBinary(data, binaryString)); - } - - public Payload(Object data) { - this(Status.OK, data); - } - - public int getStatus() { - return status.ordinal(); - } - - public boolean getKeepCallback() { - return keepAlive; - } - - public void setKeepCallback(boolean keepAlive) { - this.keepAlive = keepAlive; - } - - public JSONObject toJSON(long id, long generation) throws JSONException { - JSONObject result = new JSONObject(); - result.put("id", id); - result.put("generation", generation); - result.put("type", status == Status.OK ? 0 : 1); - result.put("status", status.ordinal()); - result.put("keep", keepAlive); - result.put("data", encode(data)); - return result; - } - - private static Object encode(Object value) throws JSONException { - if (value == null) return JSONObject.NULL; - if (value instanceof byte[]) return encodeBinary((byte[]) value, false); - if (value instanceof List) { - JSONArray parts = new JSONArray(); - for (Object part : (List) value) - parts.put(encode(((Payload) part).data)); - return new JSONObject().put("kind", "multipart").put("data", parts); - } - return value; - } - - private static JSONObject encodeBinary(byte[] bytes, boolean binaryString) { - try { - return new JSONObject() - .put("kind", binaryString ? "binaryString" : "arrayBuffer") - .put("data", Base64.encodeToString(bytes, Base64.NO_WRAP)); - } catch (JSONException exception) { - throw new IllegalArgumentException(exception); - } - } -} diff --git a/platforms/android/app/src/main/java/runner/PermissionHelper.java b/platforms/android/app/src/main/java/runner/PermissionHelper.java deleted file mode 100644 index b10911e..0000000 --- a/platforms/android/app/src/main/java/runner/PermissionHelper.java +++ /dev/null @@ -1,14 +0,0 @@ -package runner; - -public final class PermissionHelper { - private PermissionHelper() {} - public static boolean hasPermission(Service service, String permission) { - return service.host.hasPermission(permission); - } - public static void requestPermission(Service service, int code, String permission) { - service.host.requestPermission(service, code, permission); - } - public static void requestPermissions(Service service, int code, String[] permissions) { - service.host.requestPermissions(service, code, permissions); - } -} diff --git a/platforms/android/app/src/main/java/runner/Preferences.java b/platforms/android/app/src/main/java/runner/Preferences.java deleted file mode 100644 index 4469eb4..0000000 --- a/platforms/android/app/src/main/java/runner/Preferences.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ - -package runner; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import runner.LOG; - -import android.app.Activity; -import android.os.Bundle; - -public class Preferences { - private HashMap prefs = new HashMap(20); - private Bundle preferencesBundleExtras; - - public void setPreferencesBundle(Bundle extras) { - preferencesBundleExtras = extras; - } - - public void set(String name, String value) { - prefs.put(name.toLowerCase(Locale.ENGLISH), value); - } - - public void set(String name, boolean value) { - set(name, "" + value); - } - - public void set(String name, int value) { - set(name, "" + value); - } - - public void set(String name, double value) { - set(name, "" + value); - } - - public Map getAll() { - return prefs; - } - - public boolean getBoolean(String name, boolean defaultValue) { - name = name.toLowerCase(Locale.ENGLISH); - String value = prefs.get(name); - if (value != null) { - return Boolean.parseBoolean(value); - } - return defaultValue; - } - - // Added in 4.0.0 - public boolean contains(String name) { - return getString(name, null) != null; - } - - public int getInteger(String name, int defaultValue) { - name = name.toLowerCase(Locale.ENGLISH); - String value = prefs.get(name); - if (value != null) { - // Some 32-bit hex values (for example, 0x80000000) are valid int bit patterns - // but exceed Integer.MAX_VALUE when read as positive numbers. Integer.decode() - // rejects such values with NumberFormatException, so decode as long first and - // cast to int to preserve the intended 32-bit value. - return (int)(long)Long.decode(value); - } - return defaultValue; - } - - public double getDouble(String name, double defaultValue) { - name = name.toLowerCase(Locale.ENGLISH); - String value = prefs.get(name); - if (value != null) { - return Double.valueOf(value); - } - return defaultValue; - } - - public String getString(String name, String defaultValue) { - name = name.toLowerCase(Locale.ENGLISH); - String value = prefs.get(name); - if (value != null) { - return value; - } - return defaultValue; - } - -} diff --git a/platforms/android/app/src/main/java/runner/ResourceApi.java b/platforms/android/app/src/main/java/runner/ResourceApi.java deleted file mode 100644 index 1d3e8c5..0000000 --- a/platforms/android/app/src/main/java/runner/ResourceApi.java +++ /dev/null @@ -1,496 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ -package runner; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Looper; -import android.util.Base64; -import android.webkit.MimeTypeMap; - -import java.io.ByteArrayInputStream; -import java.io.File; -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 java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.channels.FileChannel; -import java.util.Locale; -import java.util.zip.GZIPInputStream; - -/** - * What this class provides: - * - *
    - *
  1. - * Helpers for reading & writing to URLs. - *
      - *
    • E.g. handles assets, resources, content providers, files, data URIs, http[s]
    • - *
    • E.g. Can be used to query for mime-type & content length.

    • - *
    - *
  2. - *
  3. - * To allow plugins to redirect URLs (via remapUrl). - *
      - *
    • - * All plugins should call remapUrl() on URLs they receive from JS *before* passing the URL onto other utility functions in this class. - *
    • - *
    • For an example usage of this, refer to the app.acode.file plugin.
    • - *
    - *
  4. - *
- * - *

Future Work:

- *
    - *
  • Consider using a Cursor to query content URLs for their size (like the file plugin does).
  • - *
  • - * Allow plugins to remapUri to "cdv-plugin://plugin-name/foo", which ResourceApi would then delegate to pluginManager.getService(plugin-name).openForRead(url) - *
      - *
    • Currently, plugins *can* do this by remapping to a data: URL, but it's inefficient for large payloads.
    • - *
    - *
  • - *
- */ -public class ResourceApi { - @SuppressWarnings("unused") - private static final String LOG_TAG = "ResourceApi"; - - public static final int URI_TYPE_FILE = 0; - public static final int URI_TYPE_ASSET = 1; - public static final int URI_TYPE_CONTENT = 2; - public static final int URI_TYPE_RESOURCE = 3; - public static final int URI_TYPE_DATA = 4; - public static final int URI_TYPE_HTTP = 5; - public static final int URI_TYPE_HTTPS = 6; - public static final int URI_TYPE_PLUGIN = 7; - public static final int URI_TYPE_UNKNOWN = -1; - - public static final String PLUGIN_URI_SCHEME = "cdvplugin"; - - private static final String[] LOCAL_FILE_PROJECTION = { "_data" }; - - public static Thread jsThread; - - private final AssetManager assetManager; - private final ContentResolver contentResolver; - private final Bridge pluginManager; - private boolean threadCheckingEnabled = true; - - - public ResourceApi(Context context, Bridge pluginManager) { - this.contentResolver = context.getContentResolver(); - this.assetManager = context.getAssets(); - this.pluginManager = pluginManager; - } - - public void setThreadCheckingEnabled(boolean value) { - threadCheckingEnabled = value; - } - - public boolean isThreadCheckingEnabled() { - return threadCheckingEnabled; - } - - - public static int getUriType(Uri uri) { - assertNonRelative(uri); - String scheme = uri.getScheme(); - if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme)) { - return URI_TYPE_CONTENT; - } - if (ContentResolver.SCHEME_ANDROID_RESOURCE.equalsIgnoreCase(scheme)) { - return URI_TYPE_RESOURCE; - } - if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme)) { - if (uri.getPath().startsWith("/android_asset/")) { - return URI_TYPE_ASSET; - } - return URI_TYPE_FILE; - } - if ("data".equalsIgnoreCase(scheme)) { - return URI_TYPE_DATA; - } - if ("http".equalsIgnoreCase(scheme)) { - return URI_TYPE_HTTP; - } - if ("https".equalsIgnoreCase(scheme)) { - return URI_TYPE_HTTPS; - } - if (PLUGIN_URI_SCHEME.equalsIgnoreCase(scheme)) { - return URI_TYPE_PLUGIN; - } - return URI_TYPE_UNKNOWN; - } - - public Uri remapUri(Uri uri) { - assertNonRelative(uri); - Uri pluginUri = pluginManager.remapUri(uri); - return pluginUri != null ? pluginUri : uri; - } - - public String remapPath(String path) { - return remapUri(Uri.fromFile(new File(path))).getPath(); - } - - /** - * @return A file that points to the resource, or null if the resource is not on the local filesystem. - */ - public File mapUriToFile(Uri uri) { - assertBackgroundThread(); - switch (getUriType(uri)) { - case URI_TYPE_FILE: - return new File(uri.getPath()); - case URI_TYPE_CONTENT: { - Cursor cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null); - if (cursor != null) { - try { - int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]); - if (columnIndex != -1 && cursor.getCount() > 0) { - cursor.moveToFirst(); - String realPath = cursor.getString(columnIndex); - if (realPath != null) { - return new File(realPath); - } - } - } finally { - cursor.close(); - } - } - } - } - return null; - } - - public String getMimeType(Uri uri) { - switch (getUriType(uri)) { - case URI_TYPE_FILE: - case URI_TYPE_ASSET: - return getMimeTypeFromPath(uri.getPath()); - case URI_TYPE_CONTENT: - case URI_TYPE_RESOURCE: - return contentResolver.getType(uri); - case URI_TYPE_DATA: { - return getDataUriMimeType(uri); - } - case URI_TYPE_HTTP: - case URI_TYPE_HTTPS: { - try { - HttpURLConnection conn = (HttpURLConnection)new URL(uri.toString()).openConnection(); - conn.setDoInput(false); - conn.setRequestMethod("HEAD"); - String mimeType = conn.getHeaderField("Content-Type"); - if (mimeType != null) { - mimeType = mimeType.split(";")[0]; - } - return mimeType; - } catch (IOException e) { - } - } - } - - return null; - } - - - //This already exists - private String getMimeTypeFromPath(String path) { - String extension = path; - int lastDot = extension.lastIndexOf('.'); - if (lastDot != -1) { - extension = extension.substring(lastDot + 1); - } - // Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185). - extension = extension.toLowerCase(Locale.getDefault()); - if (extension.equals("3ga")) { - return "audio/3gpp"; - } else if (extension.equals("js")) { - // Missing from the map :(. - return "text/javascript"; - } - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - - /** - * Opens a stream to the given URI, also providing the MIME type & length. - * - * @return Never returns null. - * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before - * being passed into this function. - * @throws IOException If the URI cannot be opened. - * @throws IllegalStateException If called on a foreground thread. - */ - public OpenForReadResult openForRead(Uri uri) throws IOException { - return openForRead(uri, false); - } - - /** - * Opens a stream to the given URI, also providing the MIME type & length. - * - * @return Never returns null. - * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before - * being passed into this function. - * @throws IOException If the URI cannot be opened. - * @throws IllegalStateException If called on a foreground thread and skipThreadCheck is false. - */ - public OpenForReadResult openForRead(Uri uri, boolean skipThreadCheck) throws IOException { - if (!skipThreadCheck) { - assertBackgroundThread(); - } - switch (getUriType(uri)) { - case URI_TYPE_FILE: { - FileInputStream inputStream = new FileInputStream(uri.getPath()); - String mimeType = getMimeTypeFromPath(uri.getPath()); - long length = inputStream.getChannel().size(); - return new OpenForReadResult(uri, inputStream, mimeType, length, null); - } - case URI_TYPE_ASSET: { - String assetPath = uri.getPath().substring(15); - AssetFileDescriptor assetFd = null; - InputStream inputStream; - long length = -1; - try { - assetFd = assetManager.openFd(assetPath); - inputStream = assetFd.createInputStream(); - length = assetFd.getLength(); - } catch (FileNotFoundException e) { - // Will occur if the file is compressed. - inputStream = assetManager.open(assetPath); - length = inputStream.available(); - } - String mimeType = getMimeTypeFromPath(assetPath); - return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); - } - case URI_TYPE_CONTENT: - case URI_TYPE_RESOURCE: { - String mimeType = contentResolver.getType(uri); - AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, "r"); - InputStream inputStream = assetFd.createInputStream(); - long length = assetFd.getLength(); - return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); - } - case URI_TYPE_DATA: { - OpenForReadResult ret = readDataUri(uri); - if (ret == null) { - break; - } - return ret; - } - case URI_TYPE_HTTP: - case URI_TYPE_HTTPS: { - HttpURLConnection conn = (HttpURLConnection)new URL(uri.toString()).openConnection(); - conn.setRequestProperty("Accept-Encoding", "gzip"); - conn.setDoInput(true); - String mimeType = conn.getHeaderField("Content-Type"); - if (mimeType != null) { - mimeType = mimeType.split(";")[0]; - } - int length = conn.getContentLength(); - InputStream inputStream; - if ("gzip".equals(conn.getContentEncoding())) { - inputStream = new GZIPInputStream(conn.getInputStream()); - } else { - inputStream = conn.getInputStream(); - } - return new OpenForReadResult(uri, inputStream, mimeType, length, null); - } - case URI_TYPE_PLUGIN: { - String pluginId = uri.getHost(); - Service plugin = pluginManager.getService(pluginId); - if (plugin == null) { - throw new FileNotFoundException("Invalid plugin ID in URI: " + uri); - } - return plugin.openForRead(uri); - } - } - throw new FileNotFoundException("URI not supported by ResourceApi: " + uri); - } - - public OutputStream openOutputStream(Uri uri) throws IOException { - return openOutputStream(uri, false); - } - - /** - * Opens a stream to the given URI. - * - * @return Never returns null. - * @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before - * being passed into this function. - * @throws IOException If the URI cannot be opened. - */ - public OutputStream openOutputStream(Uri uri, boolean append) throws IOException { - assertBackgroundThread(); - switch (getUriType(uri)) { - case URI_TYPE_FILE: { - File localFile = new File(uri.getPath()); - File parent = localFile.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - return new FileOutputStream(localFile, append); - } - case URI_TYPE_CONTENT: - case URI_TYPE_RESOURCE: { - AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, append ? "wa" : "w"); - return assetFd.createOutputStream(); - } - } - throw new FileNotFoundException("URI not supported by ResourceApi: " + uri); - } - - public HttpURLConnection createHttpConnection(Uri uri) throws IOException { - assertBackgroundThread(); - return (HttpURLConnection)new URL(uri.toString()).openConnection(); - } - - // Copies the input to the output in the most efficient manner possible. - // Closes both streams. - public void copyResource(OpenForReadResult input, OutputStream outputStream) throws IOException { - assertBackgroundThread(); - try { - InputStream inputStream = input.inputStream; - if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) { - FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel(); - FileChannel outChannel = ((FileOutputStream)outputStream).getChannel(); - long offset = 0; - long length = input.length; - if (input.assetFd != null) { - offset = input.assetFd.getStartOffset(); - } - // transferFrom()'s 2nd arg is a relative position. Need to set the absolute - // position first. - inChannel.position(offset); - outChannel.transferFrom(inChannel, 0, length); - } else { - final int BUFFER_SIZE = 8192; - byte[] buffer = new byte[BUFFER_SIZE]; - - for (;;) { - int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); - - if (bytesRead <= 0) { - break; - } - outputStream.write(buffer, 0, bytesRead); - } - } - } finally { - input.inputStream.close(); - if (outputStream != null) { - outputStream.close(); - } - } - } - - public void copyResource(Uri sourceUri, OutputStream outputStream) throws IOException { - copyResource(openForRead(sourceUri), outputStream); - } - - // Added in 3.5.0. - public void copyResource(Uri sourceUri, Uri dstUri) throws IOException { - copyResource(openForRead(sourceUri), openOutputStream(dstUri)); - } - - private void assertBackgroundThread() { - if (threadCheckingEnabled) { - Thread curThread = Thread.currentThread(); - if (curThread == Looper.getMainLooper().getThread()) { - throw new IllegalStateException("Do not perform IO operations on the UI thread. Use Host.getThreadPool() instead."); - } - if (curThread == jsThread) { - throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use Host.getThreadPool() instead."); - } - } - } - - private String getDataUriMimeType(Uri uri) { - String uriAsString = uri.getSchemeSpecificPart(); - int commaPos = uriAsString.indexOf(','); - if (commaPos == -1) { - return null; - } - String[] mimeParts = uriAsString.substring(0, commaPos).split(";"); - if (mimeParts.length > 0) { - return mimeParts[0]; - } - return null; - } - - private OpenForReadResult readDataUri(Uri uri) { - String uriAsString = uri.getSchemeSpecificPart(); - int commaPos = uriAsString.indexOf(','); - if (commaPos == -1) { - return null; - } - String[] mimeParts = uriAsString.substring(0, commaPos).split(";"); - String contentType = null; - boolean base64 = false; - if (mimeParts.length > 0) { - contentType = mimeParts[0]; - } - for (int i = 1; i < mimeParts.length; ++i) { - if ("base64".equalsIgnoreCase(mimeParts[i])) { - base64 = true; - } - } - String dataPartAsString = uriAsString.substring(commaPos + 1); - byte[] data; - if (base64) { - data = Base64.decode(dataPartAsString, Base64.DEFAULT); - } else { - try { - data = dataPartAsString.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - data = dataPartAsString.getBytes(); - } - } - InputStream inputStream = new ByteArrayInputStream(data); - return new OpenForReadResult(uri, inputStream, contentType, data.length, null); - } - - private static void assertNonRelative(Uri uri) { - if (!uri.isAbsolute()) { - throw new IllegalArgumentException("Relative URIs are not supported."); - } - } - - public static final class OpenForReadResult { - public final Uri uri; - public final InputStream inputStream; - public final String mimeType; - public final long length; - public final AssetFileDescriptor assetFd; - - public OpenForReadResult(Uri uri, InputStream inputStream, String mimeType, long length, AssetFileDescriptor assetFd) { - this.uri = uri; - this.inputStream = inputStream; - this.mimeType = mimeType; - this.length = length; - this.assetFd = assetFd; - } - } -} diff --git a/platforms/android/app/src/main/java/runner/Service.java b/platforms/android/app/src/main/java/runner/Service.java deleted file mode 100644 index 1ef9060..0000000 --- a/platforms/android/app/src/main/java/runner/Service.java +++ /dev/null @@ -1,71 +0,0 @@ -package runner; - -import android.content.Intent; -import android.net.Uri; -import java.io.IOException; -import org.json.JSONArray; -import org.json.JSONException; -import runner.webView.AppView; - -public abstract class Service { - - public Host host; - public AppView webView; - protected Preferences preferences; - - public void initialize(Host host, AppView webView) { - this.host = host; - this.webView = webView; - this.preferences = host.getPreferences(); - } - - protected void serviceInitialize() {} - - public boolean execute(String action, String args, Callback callback) - throws JSONException { - return execute(action, new JSONArray(args), callback); - } - - public boolean execute(String action, JSONArray args, Callback callback) - throws JSONException { - return false; - } - - public void onRequestPermissionResult( - int requestCode, - String[] permissions, - int[] grants - ) throws JSONException {} - - public void onActivityResult(int requestCode, int resultCode, Intent data) {} - - public Object onMessage(String id, Object data) { - return null; - } - - public void onNewIntent(Intent intent) {} - - public void onPause(boolean multitasking) {} - - public void onResume(boolean multitasking) {} - - public void onReset() {} - - public void onConfigurationChanged( - android.content.res.Configuration configuration - ) {} - - public void onDestroy() {} - - public Uri remapUri(Uri uri) { - return null; - } - - public ServicePathHandler getPathHandler() { - return null; - } - - public ResourceApi.OpenForReadResult openForRead(Uri uri) throws IOException { - throw new java.io.FileNotFoundException(uri.toString()); - } -} diff --git a/platforms/android/app/src/main/java/runner/SystemBarPlugin.java b/platforms/android/app/src/main/java/runner/SystemBarPlugin.java deleted file mode 100644 index e17cb3c..0000000 --- a/platforms/android/app/src/main/java/runner/SystemBarPlugin.java +++ /dev/null @@ -1,364 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ - -package runner; - -import runner.webView.AppView; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Color; -import android.os.Build; -import android.view.View; -import android.view.ViewParent; -import android.view.Window; -import android.view.WindowInsetsController; -import android.widget.FrameLayout; - -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsControllerCompat; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.Objects; - -public class SystemBarPlugin extends Service { - static final String PLUGIN_NAME = "SystemBarPlugin"; - - // Internal variables - private Context context; - private Resources resources; - private Integer overrideStatusBarBackgroundColor = null; - - private boolean canEdgeToEdge = false; - - @Override - protected void serviceInitialize() { - context = host.getContext(); - resources = context.getResources(); - canEdgeToEdge = preferences.getBoolean("AndroidEdgeToEdge", false); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - host.getActivity().runOnUiThread(this::updateSystemBars); - } - - @Override - public void onResume(boolean multitasking) { - super.onResume(multitasking); - host.getActivity().runOnUiThread(this::updateSystemBars); - } - - @Override - public Object onMessage(String id, Object data) { - if (id.equals("updateSystemBars")) { - host.getActivity().runOnUiThread(this::updateSystemBars); - } - return null; - } - - @Override - public boolean execute(String action, JSONArray args, Callback callbackContext) throws JSONException { - if ("setStatusBarVisible".equals(action)) { - boolean visible = args.getBoolean(0); - host.getActivity().runOnUiThread(() -> setStatusBarVisible(visible)); - } else if ("setStatusBarBackgroundColor".equals(action)) { - host.getActivity().runOnUiThread(() -> setStatusBarBackgroundColor(args)); - } else { - return false; - } - - callbackContext.success(); - return true; - } - - /** - * Allow the app to override the status bar visibility from JS API. - * If for some reason the statusBarView could not be discovered, it will silently ignore - * the change request - * - * @param visible should the status bar be visible? - */ - private void setStatusBarVisible(final boolean visible) { - View statusBar = getStatusBarView(webView); - if (statusBar != null) { - statusBar.setVisibility(visible ? View.VISIBLE : View.GONE); - - FrameLayout rootLayout = getRootLayout(webView); - if (rootLayout != null) { - ViewCompat.requestApplyInsets(rootLayout); - } - } - } - - /** - * Allow the app to override the status bar background color from JS API. - * If the supplied ARGB is invalid or fails to parse, it will silently ignore - * the change request. - * - * @param argbVals {R, G, B, A} - */ - private void setStatusBarBackgroundColor(JSONArray argbVals) { - try { - int r = argbVals.getInt(0); - int g = argbVals.getInt(1); - int b = argbVals.getInt(2); - int a = Math.round(255 * (float)argbVals.optDouble(3, 1.0)); - - overrideStatusBarBackgroundColor = Color.argb(a, r, g, b); - updateStatusBar(overrideStatusBarBackgroundColor); - } catch (JSONException e) { - // Silently skip - } - } - - /** - * Attempt to update all system bars (status, navigation and gesture bars) in various points - * of the apps life cycle. - * For example: - * 1. Device configurations between (E.g. between dark and light mode) - * 2. User resumes the app - * 3. App transitions from SplashScreen Theme to App's Theme - */ - private void updateSystemBars() { - // Update Root View Background Color - Integer rootViewBackgroundColor = getPreferenceBackgroundColor(); - if (rootViewBackgroundColor == null) { - rootViewBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor(); - } - updateRootView(rootViewBackgroundColor); - - // Update StatusBar Background Color - Integer statusBarBackgroundColor; - if (overrideStatusBarBackgroundColor != null) { - statusBarBackgroundColor = overrideStatusBarBackgroundColor; - } else if (preferences.contains("StatusBarBackgroundColor")) { - statusBarBackgroundColor = getPreferenceStatusBarBackgroundColor(); - } else if (preferences.contains("BackgroundColor")) { - statusBarBackgroundColor = rootViewBackgroundColor; - } else { - statusBarBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor(); - } - - updateStatusBar(statusBarBackgroundColor); - } - - /** - * Updates the root layout's background color with the supplied color int. - * It will also determine if the background color is light or dark to properly adjust the - * appearance of the navigation/gesture bar's icons so it will not clash with the background. - *

- * System bars (navigation & gesture) on SDK 25 or lower is forced to black as the appearance - * of the fonts can not be updated. - * System bars (navigation & gesture) on SDK 26 or greater allows custom background color. - *

- * - * @param bgColor Background color - */ - @SuppressWarnings("deprecation") - private void updateRootView(int bgColor) { - Window window = host.getActivity().getWindow(); - - // Set the root view's background color. Works on SDK 36+ - View root = host.getActivity().findViewById(android.R.id.content); - if (root != null) root.setBackgroundColor(bgColor); - - // Automatically set the font and icon color of the system bars based on background color. - boolean isBackgroundColorLight; - if(bgColor == Color.TRANSPARENT) { - isBackgroundColorLight = isColorLight(getUiModeColor()); - } else { - isBackgroundColorLight = isColorLight(bgColor); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - WindowInsetsController controller = window.getInsetsController(); - if (controller != null) { - int appearance = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - if (isBackgroundColorLight) { - controller.setSystemBarsAppearance(0, appearance); - } else { - controller.setSystemBarsAppearance(appearance, appearance); - } - } - } - WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); - controllerCompat.setAppearanceLightNavigationBars(isBackgroundColorLight); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - window.setNavigationBarColor(bgColor); - } else { - window.setNavigationBarColor(Color.BLACK); - } - } - - /** - * Updates the statusBarView background color with the supplied color int. - * It will also determine if the background color is light or dark to properly adjust the - * appearance of the status bar so the font will not clash with the background. - * - * @param bgColor Background color - */ - private void updateStatusBar(int bgColor) { - Window window = host.getActivity().getWindow(); - - View statusBar = getStatusBarView(webView); - if (statusBar != null) { - statusBar.setBackgroundColor(bgColor); - } - - // Automatically set the font and icon color of the system bars based on background color. - boolean isStatusBarBackgroundColorLight; - if(bgColor == Color.TRANSPARENT) { - isStatusBarBackgroundColorLight = isColorLight(getUiModeColor()); - } else { - isStatusBarBackgroundColorLight = isColorLight(bgColor); - } - WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); - controllerCompat.setAppearanceLightStatusBars(isStatusBarBackgroundColorLight); - } - - /** - * Determines if the supplied color's appearance is light. - * - * @param color color - * @return boolean value true is returned when the color is light. - */ - private static boolean isColorLight(int color) { - double r = Color.red(color) / 255.0; - double g = Color.green(color) / 255.0; - double b = Color.blue(color) / 255.0; - double luminance = 0.299 * r + 0.587 * g + 0.114 * b; - return luminance > 0.5; - } - - /** - * Returns the StatusBarBackgroundColor preference value or {@link #getUiModeColor()} as fallback. - * - * @return Integer - */ - private Integer getPreferenceStatusBarBackgroundColor() { - String colorString = preferences.getString("StatusBarBackgroundColor", null); - return Objects.requireNonNullElse(parseColorFromString(colorString), getUiModeColor()); - } - - /** - * Returns the BackgroundColor preference value. - * If the value is missing or fails to decode, null is returned. - * - * @return Integer|null - */ - private Integer getPreferenceBackgroundColor() { - if (!preferences.contains("BackgroundColor")) { - return null; - } - - try { - return preferences.getInteger("BackgroundColor", 0); - } catch (NumberFormatException e) { - LOG.e(PLUGIN_NAME, "Invalid background color argument. Example valid string: '0x00000000'"); - return null; - } - } - - /** - * Tries to find and return the rootLayout. - * - * @param webView AppView - * @return FrameLayout|null - */ - private FrameLayout getRootLayout(AppView webView) { - ViewParent parent = webView.getView().getParent(); - if (parent instanceof FrameLayout) { - return (FrameLayout) parent; - } - - return null; - } - - /** - * Tries to find and return the statusBarView. - * - * @param webView AppView - * @return View|null - */ - private View getStatusBarView(AppView webView) { - FrameLayout rootView = getRootLayout(webView); - if (rootView == null) { - return null; - } - - for (int i = 0; i < rootView.getChildCount(); i++) { - View child = rootView.getChildAt(i); - Object tag = child.getTag(); - if ("statusBarView".equals(tag)) { - return child; - } - } - return null; - } - - /** - * Determines the background color for status bar & root layer. - * The color will come from the app's R.color.cdv_background_color. - * If for some reason the resource is missing, it will try to fallback on the uiMode. - *

- * The uiMode as follows. - * If night mode: "#121318" (android.R.color.system_background_dark) - * If day mode: "#FAF8FF" (android.R.color.system_background_light) - * If all fails, light mode will be returned. - *

- * The hex values are supplied instead of "android.R.color" for backwards compatibility. - * - * @return int color - */ - @SuppressLint("DiscouragedApi") - private int getUiModeColor() { - boolean isNightMode = (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - String fallbackColor = isNightMode ? "#121318" : "#FAF8FF"; - int colorResId = resources.getIdentifier("cdv_background_color", "color", context.getPackageName()); - return colorResId != 0 - ? ContextCompat.getColor(context, colorResId) - : Color.parseColor(fallbackColor); - } - - /** - * Parses a color string provided by app developers. - * If the color string is empty or unable to parse, null is returned. - * - * @param colorPref hex string value, #AARRGGBB or #RRGGBB - * @return Integer|null - */ - private Integer parseColorFromString(final String colorPref) { - if (colorPref == null || colorPref.isEmpty()) return null; - - try { - return Color.parseColor(colorPref); - } catch (IllegalArgumentException ignore) { - LOG.e(PLUGIN_NAME, "Invalid color hex code. Valid format: #RRGGBB or #AARRGGBB"); - return null; - } - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/AppView.java b/platforms/android/app/src/main/java/runner/webView/AppView.java deleted file mode 100644 index b06b0e2..0000000 --- a/platforms/android/app/src/main/java/runner/webView/AppView.java +++ /dev/null @@ -1,177 +0,0 @@ -package runner.webView; - -import android.content.Context; -import android.graphics.Rect; -import android.text.InputType; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.webkit.WebView; -import runner.Bridge; -import runner.ResourceApi; - -public final class AppView extends WebView { - - private int inputType = -1; - private boolean nativeContextMenuDisabled; - private Bridge bridge; - private ResourceApi resourceApi; - private ChromeClient fullscreen; - - public AppView(Context context) { - super(context); - } - - public void initialize(Bridge bridge) { - this.bridge = bridge; - resourceApi = new ResourceApi(getContext(), bridge); - } - - public Bridge getBridge() { - return bridge; - } - - public ResourceApi getResourceApi() { - return resourceApi; - } - - public AppView getView() { - return this; - } - - public AppView getEngine() { - return this; - } - - public void setInputType(int type) { - inputType = type; - } - - public void setNativeContextMenuDisabled(boolean disabled) { - nativeContextMenuDisabled = disabled; - } - - public void setFullscreenController(ChromeClient fullscreen) { - this.fullscreen = fullscreen; - } - - public void resetChrome() { - fullscreen.reset(); - } - - public void setFullscreenBackHandler(boolean enabled) { - fullscreen.setBackHandler(enabled); - } - - public void setFullscreenOrientation(String orientation) { - fullscreen.setOrientation(orientation); - } - - public void fireDocumentEvent(String event) { - evaluateJavascript( - "window.Bridge && Bridge.fireDocumentEvent(" + - org.json.JSONObject.quote(event) + - ");", - null - ); - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo attrs) { - InputConnection connection = super.onCreateInputConnection(attrs); - if (inputType == 0) attrs.inputType |= - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; - else if (inputType == 1) attrs.inputType = - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | - InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; - return connection; - } - - @Override - public ActionMode startActionMode(ActionMode.Callback callback) { - return suppress(super.startActionMode(wrap(callback))); - } - - @Override - public ActionMode startActionMode(ActionMode.Callback callback, int type) { - return suppress(super.startActionMode(wrap(callback), type)); - } - - @Override - public ActionMode startActionModeForChild( - View child, - ActionMode.Callback callback - ) { - return suppress(super.startActionModeForChild(child, wrap(callback))); - } - - @Override - public ActionMode startActionModeForChild( - View child, - ActionMode.Callback callback, - int type - ) { - return suppress(super.startActionModeForChild(child, wrap(callback), type)); - } - - private ActionMode.Callback wrap(ActionMode.Callback callback) { - if (!nativeContextMenuDisabled || callback == null) return callback; - return new ActionMode.Callback2() { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - boolean created = callback.onCreateActionMode(mode, menu); - if (created) suppressUi(mode, menu); - return created; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - boolean prepared = callback.onPrepareActionMode(mode, menu); - suppressUi(mode, menu); - return prepared; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return callback.onActionItemClicked(mode, item); - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - callback.onDestroyActionMode(mode); - } - - @Override - public void onGetContentRect(ActionMode mode, View view, Rect rect) { - if (callback instanceof ActionMode.Callback2) ( - (ActionMode.Callback2) callback - ).onGetContentRect(mode, view, rect); - else super.onGetContentRect(mode, view, rect); - } - }; - } - - private ActionMode suppress(ActionMode mode) { - if (nativeContextMenuDisabled && mode != null) suppressUi( - mode, - mode.getMenu() - ); - return mode; - } - - private void suppressUi(ActionMode mode, Menu menu) { - if (mode == null || !nativeContextMenuDisabled || menu == null) return; - menu.clear(); - mode.setTitle(null); - mode.setSubtitle(null); - post(() -> { - if (!nativeContextMenuDisabled) return; - try { - mode.hide(0); - } catch (Throwable ignored) {} - }); - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/ChromeClient.java b/platforms/android/app/src/main/java/runner/webView/ChromeClient.java deleted file mode 100644 index bc282c3..0000000 --- a/platforms/android/app/src/main/java/runner/webView/ChromeClient.java +++ /dev/null @@ -1,220 +0,0 @@ -package runner.webView; - -import android.net.Uri; -import android.view.View; -import android.webkit.ConsoleMessage; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import android.widget.FrameLayout; -import androidx.core.view.ViewCompat; -import runner.MainActivity; - -public final class ChromeClient extends WebChromeClient { - - private final Dialogs dialogs; - private final FileChooser fileChooser; - private android.webkit.PermissionRequest pendingPermission; - private final androidx.activity.result.ActivityResultLauncher< - String[] - > permissionLauncher; - private final MainActivity activity; - private final AppView webView; - private final ImmersiveFullscreen immersive; - private CustomViewCallback fullscreenCallback; - private FrameLayout fullscreenView; - private boolean backHandler; - - public ChromeClient(MainActivity activity, AppView webView) { - this.activity = activity; - this.webView = webView; - dialogs = new Dialogs(activity); - fileChooser = new FileChooser(activity.getHost()); - permissionLauncher = activity.registerForActivityResult( - new androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions(), - grants -> { - android.webkit.PermissionRequest request = pendingPermission; - pendingPermission = null; - if (request == null) return; - if (!grants.isEmpty() && !grants.containsValue(false)) request.grant( - request.getResources() - ); - else request.deny(); - } - ); - immersive = new ImmersiveFullscreen( - activity, - true, - () -> backHandler = false - ); - } - - public boolean isFullscreen() { - return fullscreenView != null; - } - - public void pause() { - immersive.pause(); - } - - public void resume() { - immersive.resume(); - } - - public void setBackHandler(boolean enabled) { - if (enabled && !isFullscreen()) throw new IllegalStateException( - "Fullscreen is not active." - ); - backHandler = enabled; - } - - public void setOrientation(String orientation) { - if (orientation == null) immersive.unlockOrientation(); - else immersive.lockOrientation(orientation); - } - - public boolean handleBack() { - if (!isFullscreen()) return false; - if (backHandler) webView.fireDocumentEvent("fullscreenbackbutton"); - else onHideCustomView(); - return true; - } - - @Override - public void onShowCustomView(View view, CustomViewCallback callback) { - if (isFullscreen()) { - callback.onCustomViewHidden(); - return; - } - fullscreenCallback = callback; - fullscreenView = new FrameLayout(activity); - fullscreenView.setBackgroundColor(android.graphics.Color.BLACK); - fullscreenView.addView(view, new FrameLayout.LayoutParams(-1, -1)); - activity - .getContentView() - .addView(fullscreenView, new FrameLayout.LayoutParams(-1, -1)); - webView.setVisibility(View.INVISIBLE); - immersive.enter(fullscreenView); - ViewCompat.requestApplyInsets(activity.getContentView()); - } - - @Override - public void onHideCustomView() { - if (!isFullscreen()) return; - immersive.exit(); - activity.getContentView().removeView(fullscreenView); - fullscreenView = null; - webView.setVisibility(View.VISIBLE); - CustomViewCallback callback = fullscreenCallback; - fullscreenCallback = null; - callback.onCustomViewHidden(); - ViewCompat.requestApplyInsets(activity.getContentView()); - } - - @Override - public boolean onShowFileChooser( - WebView view, - ValueCallback callback, - FileChooserParams params - ) { - return fileChooser.show(view, callback, params); - } - - @Override - public boolean onJsAlert( - WebView view, - String url, - String message, - android.webkit.JsResult result - ) { - dialogs.showAlert(message, (success, value) -> { - if (success) result.confirm(); - else result.cancel(); - }); - return true; - } - - @Override - public boolean onJsConfirm( - WebView view, - String url, - String message, - android.webkit.JsResult result - ) { - dialogs.showConfirm(message, (success, value) -> { - if (success) result.confirm(); - else result.cancel(); - }); - return true; - } - - @Override - public boolean onJsPrompt( - WebView view, - String url, - String message, - String value, - android.webkit.JsPromptResult result - ) { - dialogs.showPrompt(message, value, (success, input) -> { - if (success) result.confirm(input); - else result.cancel(); - }); - return true; - } - - @Override - public void onGeolocationPermissionsShowPrompt( - String origin, - android.webkit.GeolocationPermissions.Callback callback - ) { - callback.invoke(origin, true, false); - } - - @Override - public void onPermissionRequest(android.webkit.PermissionRequest request) { - java.util.List permissions = new java.util.ArrayList<>(); - for (String resource : request.getResources()) { - if ( - resource.equals(android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE) - ) permissions.add(android.Manifest.permission.CAMERA); - if ( - resource.equals(android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE) - ) { - permissions.add(android.Manifest.permission.MODIFY_AUDIO_SETTINGS); - permissions.add(android.Manifest.permission.RECORD_AUDIO); - } - } - if (permissions.isEmpty()) request.grant(request.getResources()); - else { - if (pendingPermission != null) pendingPermission.deny(); - pendingPermission = request; - permissionLauncher.launch(permissions.toArray(new String[0])); - } - } - - @Override - public void onPermissionRequestCanceled( - android.webkit.PermissionRequest request - ) { - if (pendingPermission == request) pendingPermission = null; - } - - public void reset() { - onHideCustomView(); - dialogs.destroyLastDialog(); - } - - @Override - public boolean onConsoleMessage(ConsoleMessage message) { - android.util.Log.d( - "AcodeWebView", - message.message() + - " at " + - message.sourceId() + - ":" + - message.lineNumber() - ); - return true; - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/Dialogs.java b/platforms/android/app/src/main/java/runner/webView/Dialogs.java deleted file mode 100644 index 17f95e9..0000000 --- a/platforms/android/app/src/main/java/runner/webView/Dialogs.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ -package runner.webView; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.view.KeyEvent; -import android.widget.EditText; - -/** - * Helper class for WebViews to implement prompt(), alert(), confirm() dialogs. - */ -public class Dialogs { - private final Context context; - private AlertDialog lastHandledDialog; - - public Dialogs(Context context) { - this.context = context; - } - - public void showAlert(String message, final Result result) { - AlertDialog.Builder dlg = new AlertDialog.Builder(context); - dlg.setMessage(message); - dlg.setTitle("Alert"); - //Don't let alerts break the back button - dlg.setCancelable(true); - dlg.setPositiveButton(android.R.string.ok, - new AlertDialog.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - result.gotResult(true, null); - } - }); - dlg.setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - result.gotResult(false, null); - } - }); - dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { - //DO NOTHING - @Override - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) - { - result.gotResult(true, null); - return false; - } - else - return true; - } - }); - lastHandledDialog = dlg.show(); - } - - public void showConfirm(String message, final Result result) { - AlertDialog.Builder dlg = new AlertDialog.Builder(context); - dlg.setMessage(message); - dlg.setTitle("Confirm"); - dlg.setCancelable(true); - dlg.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - result.gotResult(true, null); - } - }); - dlg.setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - result.gotResult(false, null); - } - }); - dlg.setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - result.gotResult(false, null); - } - }); - dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { - //DO NOTHING - @Override - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) - { - result.gotResult(false, null); - return false; - } - else - return true; - } - }); - lastHandledDialog = dlg.show(); - } - - /** - * Tell the client to display a prompt dialog to the user. - * If the client returns true, WebView will assume that the client will - * handle the prompt dialog and call the appropriate JsPromptResult method. - * - *

Since we are hacking prompts for our own purposes, we should not be using them for - * this purpose, perhaps we should hack console.log to do this instead!

- */ - public void showPrompt(String message, String defaultValue, final Result result) { - // Returning false would also show a dialog, but the default one shows the origin (ugly). - AlertDialog.Builder dlg = new AlertDialog.Builder(context); - dlg.setMessage(message); - final EditText input = new EditText(context); - if (defaultValue != null) { - input.setText(defaultValue); - } - dlg.setView(input); - dlg.setCancelable(false); - dlg.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String userText = input.getText().toString(); - result.gotResult(true, userText); - } - }); - dlg.setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - result.gotResult(false, null); - } - }); - lastHandledDialog = dlg.show(); - } - - public void destroyLastDialog(){ - if (lastHandledDialog != null){ - lastHandledDialog.cancel(); - } - } - - public interface Result { - public void gotResult(boolean success, String value); - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/FileChooser.java b/platforms/android/app/src/main/java/runner/webView/FileChooser.java deleted file mode 100644 index d6783ed..0000000 --- a/platforms/android/app/src/main/java/runner/webView/FileChooser.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you 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. -*/ -package runner.webView; - -import android.app.Activity; -import android.content.*; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.provider.MediaStore; -import android.webkit.*; -import androidx.core.content.FileProvider; -import java.io.*; -import java.util.*; -import runner.Host; -import runner.Service; - -final class FileChooser { - - private static final String LOG_TAG = "AcodeFileChooser"; - private final Host host; - - FileChooser(Host host) { - this.host = host; - } - - public boolean show( - WebView webView, - final ValueCallback filePathsCallback, - final WebChromeClient.FileChooserParams fileChooserParams - ) { - Intent fileIntent = fileChooserParams.createIntent(); - - // Check if multiple-select is specified - Boolean selectMultiple = false; - if ( - fileChooserParams.getMode() == - WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE - ) { - selectMultiple = true; - } - fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple); - - // Uses Intent.EXTRA_MIME_TYPES to pass multiple mime types. - String[] acceptTypes = fileChooserParams.getAcceptTypes(); - if (acceptTypes.length > 1) { - fileIntent.setType("*/*"); // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES. - fileIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes); - } - - // Image from camera intent - Uri tempUri = null; - Intent captureIntent = null; - if (fileChooserParams.isCaptureEnabled()) { - captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - Context context = webView.getContext(); - if ( - context - .getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) && - captureIntent.resolveActivity(context.getPackageManager()) != null - ) { - try { - File tempFile = createTempFile(context); - runner.LOG.d(LOG_TAG, "Temporary photo capture file: " + tempFile); - tempUri = createUriForFile(context, tempFile); - runner.LOG.d(LOG_TAG, "Temporary photo capture URI: " + tempUri); - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri); - } catch (IOException e) { - runner.LOG.e( - LOG_TAG, - "Unable to create temporary file for photo capture", - e - ); - captureIntent = null; - } - } else { - runner.LOG.w(LOG_TAG, "Device does not support photo capture"); - captureIntent = null; - } - } - final Uri captureUri = tempUri; - - // Chooser intent - Intent chooserIntent = Intent.createChooser(fileIntent, null); - if (captureIntent != null) { - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { - captureIntent, - }); - } - - try { - runner.LOG.i(LOG_TAG, "Starting intent for file chooser"); - host.startActivityForResult( - new Service() { - @Override - public void onActivityResult( - int requestCode, - int resultCode, - Intent intent - ) { - // Handle result - Uri[] result = null; - if (resultCode == Activity.RESULT_OK) { - List uris = new ArrayList<>(); - - if (intent != null && intent.getData() != null) { - // single file - runner.LOG.v( - LOG_TAG, - "Adding file (single): " + intent.getData() - ); - uris.add(intent.getData()); - } else if (captureUri != null) { - // camera - runner.LOG.v(LOG_TAG, "Adding camera capture: " + captureUri); - uris.add(captureUri); - } else if (intent != null && intent.getClipData() != null) { - // multiple files - ClipData clipData = intent.getClipData(); - int count = clipData.getItemCount(); - for (int i = 0; i < count; i++) { - Uri uri = clipData.getItemAt(i).getUri(); - runner.LOG.v(LOG_TAG, "Adding file (multiple): " + uri); - if (uri != null) { - uris.add(uri); - } - } - } - - if (!uris.isEmpty()) { - runner.LOG.d(LOG_TAG, "Receive file chooser URL: " + uris); - result = uris.toArray(new Uri[0]); - } - } - filePathsCallback.onReceiveValue(result); - } - }, - chooserIntent, - 0 - ); - } catch (ActivityNotFoundException e) { - runner.LOG.w( - LOG_TAG, - "No activity found to handle file chooser intent.", - e - ); - filePathsCallback.onReceiveValue(null); - } - return true; - } - - private File createTempFile(Context context) throws IOException { - // Create an image file name - return File.createTempFile("temp", ".jpg", context.getCacheDir()); - } - - private Uri createUriForFile(Context context, File tempFile) - throws IOException { - String appId = context.getPackageName(); - return FileProvider.getUriForFile(context, appId + ".provider", tempFile); - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/ImmersiveFullscreen.java b/platforms/android/app/src/main/java/runner/webView/ImmersiveFullscreen.java deleted file mode 100644 index 895767e..0000000 --- a/platforms/android/app/src/main/java/runner/webView/ImmersiveFullscreen.java +++ /dev/null @@ -1,221 +0,0 @@ -package runner.webView; - -import android.app.Activity; -import android.content.pm.ActivityInfo; -import android.os.Build; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.Window; -import android.view.WindowManager; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; - -/** Owns window state only while Acode is displaying a browser fullscreen view. */ -final class ImmersiveFullscreen - implements - ViewTreeObserver.OnWindowFocusChangeListener, - View.OnAttachStateChangeListener -{ - - @SuppressWarnings("deprecation") - private static final int LEGACY_FULLSCREEN_FLAGS = - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - - private final Activity activity; - private final Window window; - private final View decor; - private final WindowInsetsControllerCompat controller; - private final Runnable onExit; - private View fullscreenView; - private ViewTreeObserver focusObserver; - private boolean statusBarVisible; - private boolean navigationBarVisible; - private int previousBehavior; - private int previousCutoutMode; - private int previousLegacyFlags; - private boolean resumed; - private Integer requestedOrientation; - private int previousOrientation; - private boolean orientationApplied; - - ImmersiveFullscreen(Activity activity, boolean resumed, Runnable onExit) { - this.activity = activity; - this.resumed = resumed; - this.onExit = onExit; - window = activity.getWindow(); - decor = window.getDecorView(); - controller = WindowCompat.getInsetsController(window, decor); - } - - @SuppressWarnings("deprecation") - void enter(View view) { - if (fullscreenView != null) return; - - WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decor); - int flags = decor.getSystemUiVisibility(); - statusBarVisible = - insets != null - ? insets.isVisible(WindowInsetsCompat.Type.statusBars()) - : (flags & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 && - (window.getAttributes().flags & - WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0; - navigationBarVisible = - insets != null - ? insets.isVisible(WindowInsetsCompat.Type.navigationBars()) - : (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; - previousBehavior = controller.getSystemBarsBehavior(); - previousLegacyFlags = flags & LEGACY_FULLSCREEN_FLAGS; - if (Build.VERSION.SDK_INT >= 28) { - WindowManager.LayoutParams attributes = window.getAttributes(); - previousCutoutMode = attributes.layoutInDisplayCutoutMode; - attributes.layoutInDisplayCutoutMode = - Build.VERSION.SDK_INT >= 30 - ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - window.setAttributes(attributes); - } - - fullscreenView = view; - // The wrapper fills Acode's root, with no status/navigation bar margins. - // Only the keyboard should shrink the usable area. Leave the root's inset - // listener alone so normal editor layout is unchanged on exit. - ViewCompat.setOnApplyWindowInsetsListener(view, (target, appliedInsets) -> { - int bottom = appliedInsets - .getInsets(WindowInsetsCompat.Type.ime()) - .bottom; - target.setPadding(0, 0, 0, bottom); - return appliedInsets; - }); - view.addOnAttachStateChangeListener(this); - focusObserver = decor.getViewTreeObserver(); - focusObserver.addOnWindowFocusChangeListener(this); - reapply(); - ViewCompat.requestApplyInsets(view); - } - - void reapply() { - if ( - !resumed || - fullscreenView == null || - !fullscreenView.isAttachedToWindow() || - !decor.hasWindowFocus() - ) return; - controller.setSystemBarsBehavior( - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - ); - controller.hide(WindowInsetsCompat.Type.systemBars()); - } - - void lockOrientation(String orientation) { - final int requested; - if ("landscape".equals(orientation)) { - requested = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - } else if ("portrait".equals(orientation)) { - requested = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - } else { - throw new IllegalArgumentException( - "Orientation must be landscape or portrait." - ); - } - if ( - !resumed || fullscreenView == null || !fullscreenView.isAttachedToWindow() - ) { - throw new IllegalStateException( - "Orientation requires foreground fullscreen." - ); - } - - int previous = activity.getRequestedOrientation(); - activity.setRequestedOrientation(requested); - // Only a successful first request owns the restoration state. Changing - // modes or resuming this session must not replace it with our own override. - if (requestedOrientation == null) previousOrientation = previous; - requestedOrientation = requested; - orientationApplied = true; - } - - void unlockOrientation() { - restoreOrientation(); - requestedOrientation = null; - } - - void pause() { - resumed = false; - restoreOrientation(); - } - - void resume() { - resumed = true; - if ( - fullscreenView != null && - fullscreenView.isAttachedToWindow() && - requestedOrientation != null - ) { - activity.setRequestedOrientation(requestedOrientation); - orientationApplied = true; - } - reapply(); - } - - private void restoreOrientation() { - if (!orientationApplied) return; - activity.setRequestedOrientation(previousOrientation); - orientationApplied = false; - } - - @SuppressWarnings("deprecation") - void exit() { - if (fullscreenView == null) return; - View view = fullscreenView; - fullscreenView = null; - onExit.run(); - unlockOrientation(); - view.removeOnAttachStateChangeListener(this); - ViewCompat.setOnApplyWindowInsetsListener(view, null); - view.setPadding(0, 0, 0, 0); - if ( - focusObserver.isAlive() - ) focusObserver.removeOnWindowFocusChangeListener(this); - focusObserver = null; - - controller.setSystemBarsBehavior(previousBehavior); - restoreBar(WindowInsetsCompat.Type.statusBars(), statusBarVisible); - restoreBar(WindowInsetsCompat.Type.navigationBars(), navigationBarVisible); - if (Build.VERSION.SDK_INT < 30) { - // Restore only the bits we own; keep any theme changes made meanwhile. - decor.setSystemUiVisibility( - (decor.getSystemUiVisibility() & ~LEGACY_FULLSCREEN_FLAGS) | - previousLegacyFlags - ); - } - if (Build.VERSION.SDK_INT >= 28) { - WindowManager.LayoutParams attributes = window.getAttributes(); - attributes.layoutInDisplayCutoutMode = previousCutoutMode; - window.setAttributes(attributes); - } - ViewCompat.requestApplyInsets(decor); - } - - private void restoreBar(int type, boolean visible) { - if (visible) controller.show(type); - else controller.hide(type); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - if (hasFocus) reapply(); - } - - @Override - public void onViewAttachedToWindow(View view) {} - - @Override - public void onViewDetachedFromWindow(View view) { - exit(); - } -} diff --git a/platforms/android/app/src/main/java/runner/webView/WebViewClient.java b/platforms/android/app/src/main/java/runner/webView/WebViewClient.java deleted file mode 100644 index 09d6c3a..0000000 --- a/platforms/android/app/src/main/java/runner/webView/WebViewClient.java +++ /dev/null @@ -1,116 +0,0 @@ -package runner.webView; - -import android.content.Intent; -import android.net.Uri; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebView; -import androidx.webkit.WebViewAssetLoader; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.URLConnection; -import java.util.Collections; -import runner.Bridge; -import runner.MainActivity; -import runner.Service; -import runner.ServicePathHandler; - -public final class WebViewClient extends android.webkit.WebViewClient { - - private final MainActivity activity; - private final Bridge bridge; - - public WebViewClient(MainActivity activity, Bridge bridge) { - this.activity = activity; - this.bridge = bridge; - } - - @Override - public void onReceivedSslError( - WebView view, - android.webkit.SslErrorHandler handler, - android.net.http.SslError error - ) { - if (com.foxdebug.acode.BuildConfig.DEBUG) handler.proceed(); - else handler.cancel(); - } - - @Override - public void onPageStarted( - WebView view, - String url, - android.graphics.Bitmap icon - ) { - ((AppView) view).resetChrome(); - bridge.reset(); - super.onPageStarted(view, url, icon); - } - - @Override - public boolean shouldOverrideUrlLoading( - WebView view, - WebResourceRequest request - ) { - Uri uri = request.getUrl(); - if ( - "https".equals(uri.getScheme()) || "http".equals(uri.getScheme()) - ) return false; - if (!request.isForMainFrame()) return false; - try { - activity.startActivity(new Intent(Intent.ACTION_VIEW, uri)); - } catch (android.content.ActivityNotFoundException ignored) {} - return true; - } - - @Override - public WebResourceResponse shouldInterceptRequest( - WebView view, - WebResourceRequest request - ) { - Uri uri = request.getUrl(); - if ( - !"https".equals(uri.getScheme()) || !"localhost".equals(uri.getHost()) - ) return null; - String path = uri.getPath(); - if (path == null || path.equals("/")) path = "/index.html"; - Service file = bridge.getService("File"); - ServicePathHandler handler = file.getPathHandler(); - WebResourceResponse response = handler - .getPathHandler() - .handle(path.substring(1)); - if (response != null) return response; - if ( - java.util.Arrays.asList(path.split("/")).contains("..") || - path.indexOf('\0') != -1 - ) return missing(); - try { - InputStream stream = activity.getAssets().open("bundle" + path); - String mime = path.endsWith(".js") - ? "application/javascript" - : path.endsWith(".wasm") - ? "application/wasm" - : URLConnection.guessContentTypeFromName(path); - if (mime == null) mime = "application/octet-stream"; - return new WebResourceResponse( - mime, - mime.startsWith("text/") || mime.equals("application/javascript") - ? "UTF-8" - : null, - stream - ); - } catch (java.io.IOException exception) { - return missing(); - } - } - - private static WebResourceResponse missing() { - return new WebResourceResponse( - "text/plain", - "UTF-8", - 404, - "Not Found", - Collections.emptyMap(), - new ByteArrayInputStream(new byte[0]) - ); - } -} diff --git a/platforms/android/app/src/paid/java/com/foxdebug/acode/runtime/EditionServices.kt b/platforms/android/app/src/paid/java/com/foxdebug/acode/runtime/EditionServices.kt new file mode 100644 index 0000000..b8cd96e --- /dev/null +++ b/platforms/android/app/src/paid/java/com/foxdebug/acode/runtime/EditionServices.kt @@ -0,0 +1,5 @@ +package com.foxdebug.acode.runtime + +internal object EditionServices : ServiceModule { + override val services: List = emptyList() +} diff --git a/platforms/android/app/src/store/AndroidManifest.xml b/platforms/android/app/src/store/AndroidManifest.xml index c08dac2..8957cde 100644 --- a/platforms/android/app/src/store/AndroidManifest.xml +++ b/platforms/android/app/src/store/AndroidManifest.xml @@ -1,4 +1,5 @@ - - + + diff --git a/platforms/android/app/src/store/java/com/foxdebug/acode/runtime/ChannelServices.kt b/platforms/android/app/src/store/java/com/foxdebug/acode/runtime/ChannelServices.kt new file mode 100644 index 0000000..b3f60bc --- /dev/null +++ b/platforms/android/app/src/store/java/com/foxdebug/acode/runtime/ChannelServices.kt @@ -0,0 +1,9 @@ +package com.foxdebug.acode.runtime + +import com.foxdebug.iap.Iap + +internal object ChannelServices : ServiceModule { + override val services: List = listOf( + ServiceDefinition(ServiceName.IAP) { Iap() }, + ) +} diff --git a/platforms/android/app/src/store/java/com/foxdebug/iap/Iap.java b/platforms/android/app/src/store/java/com/foxdebug/iap/Iap.java index 77ae710..1172a3b 100644 --- a/platforms/android/app/src/store/java/com/foxdebug/iap/Iap.java +++ b/platforms/android/app/src/store/java/com/foxdebug/iap/Iap.java @@ -2,7 +2,7 @@ import android.app.Activity; import android.content.Context; -import android.util.Log; + import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; @@ -25,11 +25,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import runner.Callback; -import runner.Host; -import runner.Service; -import runner.webView.AppView; -import runner.Payload; +import com.foxdebug.acode.runtime.BridgeContext; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.Service; +import com.foxdebug.acode.runtime.Payload; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -41,8 +40,8 @@ public class Iap extends Service { private WeakReference activityRef; private Callback purchaseUpdated; - public void initialize(Host host, AppView webView) { - super.initialize(host, webView); + public void initialize(BridgeContext bridgeContext) { + super.initialize(bridgeContext); contextRef = new WeakReference<>(host.getContext()); activityRef = new WeakReference<>(host.getActivity()); billingClient = getBillingClient(); diff --git a/platforms/android/app/src/test/java/runner/FileResourceTest.java b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/FileResourceTest.java similarity index 84% rename from platforms/android/app/src/test/java/runner/FileResourceTest.java rename to platforms/android/app/src/test/java/com/foxdebug/acode/runtime/FileResourceTest.java index e39211e..99ebd7d 100644 --- a/platforms/android/app/src/test/java/runner/FileResourceTest.java +++ b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/FileResourceTest.java @@ -1,11 +1,12 @@ -package runner; +package com.foxdebug.acode.runtime; import static org.junit.Assert.*; import android.net.Uri; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; -import com.foxdebug.acode.BuildConfig; +import com.foxdebug.acode.MainActivity; +import com.foxdebug.acode.runtime.webview.AppWebView; import java.io.File; import java.io.InputStream; import java.nio.file.Files; @@ -17,7 +18,6 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import runner.webView.AppView; @RunWith(RobolectricTestRunner.class) @Config(sdk = 29) @@ -31,10 +31,10 @@ public void servesEncodedFileNamesWithoutReinterpretingTheirCharacters() try ( org.robolectric.android.controller.ActivityController< MainActivity - > controller = Robolectric.buildActivity(activityClass()).setup() + > controller = Robolectric.buildActivity(MainActivity.class).setup() ) { MainActivity activity = controller.get(); - AppView view = (AppView) activity.getContentView().getChildAt(0); + AppWebView view = (AppWebView) activity.getContentView().getChildAt(0); android.webkit.WebViewClient client = view.getWebViewClient(); byte[] bytes = {0, 127, (byte) 128, (byte) 255}; for (String name : new String[] { @@ -80,12 +80,4 @@ public Map getRequestHeaders() { } }; } - - @SuppressWarnings("unchecked") - private static Class activityClass() - throws ClassNotFoundException { - return (Class) Class.forName( - BuildConfig.APPLICATION_ID + ".MainActivity" - ); - } } diff --git a/platforms/android/app/src/test/java/runner/PayloadTest.java b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/PayloadTest.java similarity index 98% rename from platforms/android/app/src/test/java/runner/PayloadTest.java rename to platforms/android/app/src/test/java/com/foxdebug/acode/runtime/PayloadTest.java index e69ce4b..b798feb 100644 --- a/platforms/android/app/src/test/java/runner/PayloadTest.java +++ b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/PayloadTest.java @@ -1,4 +1,4 @@ -package runner; +package com.foxdebug.acode.runtime; import static org.junit.Assert.*; diff --git a/platforms/android/app/src/test/java/runner/StartupTest.java b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/StartupTest.java similarity index 77% rename from platforms/android/app/src/test/java/runner/StartupTest.java rename to platforms/android/app/src/test/java/com/foxdebug/acode/runtime/StartupTest.java index fb672a6..7e3c244 100644 --- a/platforms/android/app/src/test/java/runner/StartupTest.java +++ b/platforms/android/app/src/test/java/com/foxdebug/acode/runtime/StartupTest.java @@ -1,9 +1,11 @@ -package runner; +package com.foxdebug.acode.runtime; import static org.junit.Assert.*; import android.os.Looper; import com.foxdebug.acode.BuildConfig; +import com.foxdebug.acode.runtime.webview.AppWebView; + import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -21,21 +23,25 @@ public void startsAndResumesWithoutReloadingTheEditor() throws Exception { Thread.getDefaultUncaughtExceptionHandler(); try ( org.robolectric.android.controller.ActivityController< - MainActivity + BaseWebActivity > controller = Robolectric.buildActivity(activityClass()) ) { - MainActivity activity = controller.create().start().resume().get(); - runner.webView.AppView view = (runner.webView.AppView) activity + BaseWebActivity activity = controller.create().start().resume().get(); + AppWebView view = (AppWebView) activity .getContentView() .getChildAt(0); assertEquals("https://localhost/index.html", view.getUrl()); android.content.pm.ActivityInfo info = activity .getPackageManager() .getActivityInfo(activity.getComponentName(), 0); - assertEquals(BuildConfig.APPLICATION_ID + ".MainActivity", info.name); - assertNotNull(view.getBridge().getService("File")); - assertNotNull(view.getBridge().getService("Authenticator")); - assertNotNull(view.getBridge().getService("System")); + assertEquals(activityClass().getName(), info.name); + assertNotNull(view.getBridge().getService(ServiceName.FILE)); + assertNotNull(view.getBridge().getService(ServiceName.AUTHENTICATOR)); + assertNotNull(view.getBridge().getService(ServiceName.SYSTEM)); + if (BuildConfig.FLAVOR_edition.equals("free")) assertNotNull( + view.getBridge().getService(ServiceName.AD_MOB) + ); + else assertNull(view.getBridge().getService(ServiceName.AD_MOB)); long generation = view.getBridge().getGeneration(); controller.pause().resume(); assertEquals(generation, view.getBridge().getGeneration()); @@ -44,6 +50,15 @@ public void startsAndResumesWithoutReloadingTheEditor() throws Exception { } } + @Test + public void exposesEveryTransportKeyOnce() { + java.util.HashSet keys = new java.util.HashSet<>(); + for (ServiceName name : ServiceName.values()) { + assertSame(name, ServiceName.fromKey(name.getKey())); + assertTrue("duplicate key " + name.getKey(), keys.add(name.getKey())); + } + } + @Test public void routesOverlappingActivityRequestsToTheirOwners() throws Exception { @@ -51,10 +66,10 @@ public void routesOverlappingActivityRequestsToTheirOwners() Thread.getDefaultUncaughtExceptionHandler(); try ( org.robolectric.android.controller.ActivityController< - MainActivity + BaseWebActivity > controller = Robolectric.buildActivity(activityClass()).setup() ) { - MainActivity activity = controller.get(); + BaseWebActivity activity = controller.get(); int[] received = new int[2]; Service first = new Service() { @Override @@ -110,10 +125,10 @@ public void discardsCallbacksFromPreviousPageGenerations() throws Exception { Thread.getDefaultUncaughtExceptionHandler(); try ( org.robolectric.android.controller.ActivityController< - MainActivity + BaseWebActivity > controller = Robolectric.buildActivity(activityClass()).setup() ) { - runner.webView.AppView view = (runner.webView.AppView) controller + AppWebView view = (AppWebView) controller .get() .getContentView() .getChildAt(0); @@ -140,15 +155,15 @@ public void hardwareBackReachesFullscreenOwnerBeforeWebView() Thread.getDefaultUncaughtExceptionHandler(); try ( org.robolectric.android.controller.ActivityController< - MainActivity + BaseWebActivity > controller = Robolectric.buildActivity(activityClass()).setup() ) { - MainActivity activity = controller.get(); - runner.webView.AppView view = (runner.webView.AppView) activity + BaseWebActivity activity = controller.get(); + AppWebView view = (AppWebView) activity .getContentView() .getChildAt(0); - runner.webView.ChromeClient chrome = - (runner.webView.ChromeClient) view.getWebChromeClient(); + com.foxdebug.acode.runtime.webview.AcodeChromeClient chrome = + (com.foxdebug.acode.runtime.webview.AcodeChromeClient) view.getWebChromeClient(); int[] hidden = {0}; chrome.onShowCustomView( new android.view.View(activity), @@ -193,10 +208,7 @@ public void hardwareBackReachesFullscreenOwnerBeforeWebView() } @SuppressWarnings("unchecked") - private static Class activityClass() - throws ClassNotFoundException { - return (Class) Class.forName( - BuildConfig.APPLICATION_ID + ".MainActivity" - ); + private static Class activityClass() { + return (Class) (Class) com.foxdebug.acode.MainActivity.class; } } diff --git a/platforms/android/app/src/test/java/com/foxdebug/websocket/WebSocketInstanceTest.java b/platforms/android/app/src/test/java/com/foxdebug/acode/websocket/WebSocketInstanceTest.java similarity index 82% rename from platforms/android/app/src/test/java/com/foxdebug/websocket/WebSocketInstanceTest.java rename to platforms/android/app/src/test/java/com/foxdebug/acode/websocket/WebSocketInstanceTest.java index e00c2da..f598d99 100644 --- a/platforms/android/app/src/test/java/com/foxdebug/websocket/WebSocketInstanceTest.java +++ b/platforms/android/app/src/test/java/com/foxdebug/acode/websocket/WebSocketInstanceTest.java @@ -1,8 +1,7 @@ -package com.foxdebug.websocket; +package com.foxdebug.acode.websocket; import static org.junit.Assert.*; -import com.foxdebug.acode.BuildConfig; import java.util.ArrayList; import java.util.List; import okhttp3.Protocol; @@ -17,10 +16,10 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import runner.Callback; -import runner.MainActivity; -import runner.Payload; -import runner.webView.AppView; +import com.foxdebug.acode.runtime.Callback; +import com.foxdebug.acode.runtime.BaseWebActivity; +import com.foxdebug.acode.runtime.Payload; +import com.foxdebug.acode.runtime.webview.AppWebView; @RunWith(RobolectricTestRunner.class) @Config(sdk = 29) @@ -29,10 +28,10 @@ public class WebSocketInstanceTest { @SuppressWarnings("unchecked") public void deliversEarlyOpenMessageAndCloseInOrder() throws Exception { Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); - Class activityClass = (Class) Class.forName(BuildConfig.APPLICATION_ID + ".MainActivity"); - try (org.robolectric.android.controller.ActivityController controller = Robolectric.buildActivity(activityClass).setup()) { - MainActivity activity = controller.get(); - AppView view = (AppView) activity.getContentView().getChildAt(0); + Class activityClass = (Class) (Class) com.foxdebug.acode.MainActivity.class; + try (org.robolectric.android.controller.ActivityController controller = Robolectric.buildActivity(activityClass).setup()) { + BaseWebActivity activity = controller.get(); + AppWebView view = (AppWebView) activity.getContentView().getChildAt(0); Request[] requests = new Request[1]; WebSocket.Factory factory = (request, listener) -> { requests[0] = request; return null; }; WebSocketInstance socket = new WebSocketInstance("ws://localhost/test", new JSONArray().put("test"), new JSONObject().put("Authorization", "token"), "arraybuffer", factory, activity.getHost(), "early"); diff --git a/platforms/android/build.gradle b/platforms/android/build.gradle deleted file mode 100644 index 6b31069..0000000 --- a/platforms/android/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'com.android.application' version '9.4.1' apply false -} diff --git a/platforms/android/build.gradle.kts b/platforms/android/build.gradle.kts new file mode 100644 index 0000000..d052920 --- /dev/null +++ b/platforms/android/build.gradle.kts @@ -0,0 +1,4 @@ +//Project level build file do not add deps here +plugins { + alias(libs.plugins.android.application) apply false +} diff --git a/platforms/android/dependencies.gradle b/platforms/android/dependencies.gradle deleted file mode 100644 index 15c32f4..0000000 --- a/platforms/android/dependencies.gradle +++ /dev/null @@ -1,19 +0,0 @@ -dependencies { - freeImplementation "com.google.android.gms:play-services-ads:25.4.0" - freeImplementation "com.google.android.ump:user-messaging-platform:4.0.0" - implementation "androidx.browser:browser:1.8.0" - implementation "androidx.core:core-google-shortcuts:1.0.0" - implementation "androidx.core:core:1.6.0" - implementation "androidx.documentfile:documentfile:1.0.1" - implementation "androidx.security:security-crypto:1.1.0" - implementation "com.squareup.okhttp3:okhttp:4.12.0" - implementation "com.sshtools:maverick-synergy-client:3.1.4" - implementation "commons-codec:commons-codec:1.10" - implementation "commons-io:commons-io:2.11.0" - implementation "commons-net:commons-net:3.12.0" - implementation "org.bouncycastle:bcpkix-jdk15to18:1.83" - implementation "org.bouncycastle:bcprov-jdk15to18:1.83" - implementation "org.java-websocket:Java-WebSocket:1.6.0" - implementation "org.nanohttpd:nanohttpd:2.3.1" - if (!providers.gradleProperty('fdroid').orElse('false').get().toBoolean()) implementation "com.android.billingclient:billing:8.0.0" -} diff --git a/platforms/android/gradle.properties b/platforms/android/gradle.properties index 4a0b1a0..abe3507 100644 --- a/platforms/android/gradle.properties +++ b/platforms/android/gradle.properties @@ -1,4 +1,9 @@ -org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 +# More than 2gb is a waste +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 android.useAndroidX=true android.nonTransitiveRClass=true + +#using all cpu cores may freeze old computer when building org.gradle.parallel=true +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/platforms/android/gradle/gradle-daemon-jvm.properties b/platforms/android/gradle/gradle-daemon-jvm.properties index a3e1a4c..a7a6ee2 100644 --- a/platforms/android/gradle/gradle-daemon-jvm.properties +++ b/platforms/android/gradle/gradle-daemon-jvm.properties @@ -1,3 +1,2 @@ #This file is generated by :updateDaemonJvm -toolchainVendor=ADOPTIUM -toolchainVersion=27 +toolchainVersion=26 diff --git a/platforms/android/gradle/libs.versions.toml b/platforms/android/gradle/libs.versions.toml index 2efd3e4..608f310 100644 --- a/platforms/android/gradle/libs.versions.toml +++ b/platforms/android/gradle/libs.versions.toml @@ -1,33 +1,54 @@ [versions] -agp = "9.2.1" -barcodeScanning = "18.11.0" -browser = "1.8.0" -cameraCamera2 = "1.4.2" -cameraLifecycle = "1.4.2" -cameraView = "1.4.2" -coreSplashscreen = "1.0.1" +agp = "9.4.1" + + +#needs more attention +billing = "8.0.0" +playServicesAds = "25.4.0" + +appcompat = "1.8.0" +bouncycastle = "1.86" +browser = "1.10.0" +commonsCodec = "1.22.1" +commonsIo = "2.22.0" +commonsNet = "3.13.0" +core = "1.19.1" +coreGoogleShortcuts = "1.1.0" +coreSplashscreen = "1.2.0" +documentfile = "1.1.0" +javaWebsocket = "1.6.0" junit = "4.13.2" -junitVersion = "1.2.1" -espressoCore = "3.6.1" -appcompat = "1.7.1" -socketIoClient = "2.1.2" -webkit = "1.14.0" +maverick = "3.1.4" +nanohttpd = "2.3.1" +okhttp = "5.5.0" +robolectric = "4.17" +securityCrypto = "1.1.0" +userMessagingPlatform = "4.0.0" +webkit = "1.17.1" [libraries] +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } -androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraLifecycle" } -androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraView" } +androidx-core = { module = "androidx.core:core", version.ref = "core" } +androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } -barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } -camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCamera2" } -common = { module = "com.google.mlkit:common", version.ref = "barcodeScanning" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -socket-io-client = { module = "io.socket:socket.io-client", version.ref = "socketIoClient" } -webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" } +androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +billing-client = { module = "com.android.billingclient:billing", version.ref = "billing" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk15to18", version.ref = "bouncycastle" } +bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" } +commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" } +commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" } +commons-net = { module = "commons-net:commons-net", version.ref = "commonsNet" } +java-websocket = { module = "org.java-websocket:Java-WebSocket", version.ref = "javaWebsocket" } +junit = { module = "junit:junit", version.ref = "junit" } +maverick-synergy-client = { module = "com.sshtools:maverick-synergy-client", version.ref = "maverick" } +nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +user-messaging-platform = { module = "com.google.android.ump:user-messaging-platform", version.ref = "userMessagingPlatform" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } - diff --git a/platforms/android/settings.gradle b/platforms/android/settings.gradle.kts similarity index 86% rename from platforms/android/settings.gradle rename to platforms/android/settings.gradle.kts index 9d1dbbd..10858f4 100644 --- a/platforms/android/settings.gradle +++ b/platforms/android/settings.gradle.kts @@ -5,6 +5,7 @@ pluginManagement { gradlePluginPortal() } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -12,5 +13,6 @@ dependencyResolutionManagement { mavenCentral() } } -rootProject.name = 'Acode' -include ':app' + +rootProject.name = "Acode" +include(":app") diff --git a/readme.md b/readme.md index 5d8b30f..8392d5c 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ Welcome to Acode Editor - a powerful and versatile code editing tool designed specifically for Android devices. Whether you're working on HTML, CSS, JavaScript, or other programming languages, Acode empowers you to code on-the-go with confidence. -Acode runs in an Android WebView with its own native service bridge. Android sources are tracked directly; native integrations are vendored and built with Java 27, the Gradle 9.8 RC3 wrapper and Android Gradle Plugin 9.4.1. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and build commands. +Acode runs in an Android WebView with its own native service bridge. Android sources are tracked directly; native integrations are vendored and built with Java 26, the Gradle 9.8 RC3 wrapper and Android Gradle Plugin 9.4.1. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and build commands. An iOS port is in progress using the same editor and plugin APIs with Swift native services. It is not release-ready; current coverage and remaining features are tracked in [docs/ios-port.md](docs/ios-port.md). diff --git a/tests/unit/appIcons.test.js b/tests/unit/appIcons.test.js index 69c27aa..e790ecd 100644 --- a/tests/unit/appIcons.test.js +++ b/tests/unit/appIcons.test.js @@ -17,7 +17,7 @@ describe("appIcons", () => { it("keeps picker IDs, native mappings, and launcher aliases in sync", () => { const native = fs.readFileSync( new URL( - "../../platforms/android/app/src/main/java/com/foxdebug/system/System.java", + "../../platforms/android/app/src/main/java/com/foxdebug/acode/system/System.java", import.meta.url, ), "utf8", @@ -35,9 +35,14 @@ describe("appIcons", () => { "application/xml", ); expect( - [...config.querySelectorAll("activity-alias")] - .map((alias) => alias.getAttribute("android:name")), + [...config.querySelectorAll("activity-alias")].map((alias) => + resolveAlias(alias.getAttribute("android:name")), + ), ).toEqual(mappings.map(([, name]) => `\${applicationId}.${name}`)); window.happyDOM.abort(); }); }); + +function resolveAlias(name) { + return name.startsWith(".") ? `\${applicationId}${name}` : name; +} diff --git a/tests/unit/dev.test.js b/tests/unit/dev.test.js index 9f27ddc..e77a595 100644 --- a/tests/unit/dev.test.js +++ b/tests/unit/dev.test.js @@ -21,7 +21,7 @@ test.each([ ); expect(getAppConfig()).toEqual({ targetId: name, variant }); expect(parseOptions(["android", "prod", "bundle", "fdroid", "--target=device"])).toMatchObject({ - targetId: name, variant, mode: "p", bundle: true, fdroid: true, target: "device", + targetId: name, variant, mode: "p", bundle: true, channel: "fdroid", target: "device", }); expect(parseIOSOptions(["ios", "prod", "--target=simulator"])).toMatchObject({ targetId: name, variant, mode: "Release", target: "simulator", device: false, diff --git a/tests/unit/nativeSearchProcessing.test.js b/tests/unit/nativeSearchProcessing.test.js index e15bd3d..93bc8e6 100644 --- a/tests/unit/nativeSearchProcessing.test.js +++ b/tests/unit/nativeSearchProcessing.test.js @@ -10,7 +10,7 @@ it.skipIf(!hasJava)( () => { const source = readFileSync( new URL( - "../../platforms/android/app/src/main/java/com/foxdebug/sdcard/WorkspaceIndex.java", + "../../platforms/android/app/src/main/java/com/foxdebug/acode/sdcard/WorkspaceIndex.java", import.meta.url, ), "utf8",