├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── fastlane.yml │ ├── lint.yml │ ├── release.yml │ └── report_test_results.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ ├── AndroidManifest.xml │ ├── java │ │ └── rs │ │ │ └── ruffle │ │ │ ├── InputEvents.kt │ │ │ └── SmokeTest.kt │ └── res │ │ └── raw │ │ ├── helloflash.swf │ │ ├── input_test.swf │ │ └── input_tests │ │ ├── Test.as │ │ ├── test.fla │ │ └── test.swf │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── rs │ │ │ └── ruffle │ │ │ ├── MainActivity.kt │ │ │ ├── Navigation.kt │ │ │ ├── PanicActivity.kt │ │ │ ├── PanicScreen.kt │ │ │ ├── PlayerActivity.kt │ │ │ ├── SelectSwfScreen.kt │ │ │ ├── TextFieldState.kt │ │ │ ├── UrlState.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ └── ic_logo_dark.xml │ │ ├── layout │ │ └── keyboard.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── rs │ └── ruffle │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ └── icon.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── rust-toolchain.toml ├── settings.gradle.kts └── src ├── audio.rs ├── custom_event.rs ├── java.rs ├── keycodes.rs ├── lib.rs ├── navigator.rs └── trace.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,kts}] 4 | ktlint_code_style = android_studio 5 | ij_kotlin_name_count_to_use_star_import = 9999 6 | ij_kotlin_name_count_to_use_star_import_for_members = 9999 7 | ktlint_function_naming_ignore_when_annotated_with=Composable 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | allow: 8 | - dependency-type: "all" 9 | groups: 10 | rust-minor: 11 | patterns: 12 | - "*" 13 | update-types: 14 | - "minor" 15 | - "patch" 16 | ruffle: 17 | patterns: 18 | - "ruffle*" 19 | - package-ecosystem: "gradle" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | groups: 24 | gradle-minor: 25 | patterns: 26 | - "*" 27 | update-types: 28 | - "minor" 29 | - "patch" 30 | - package-ecosystem: "github-actions" 31 | directory: "/" 32 | schedule: 33 | interval: "weekly" 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | NDK_VERSION: "r27" 13 | CARGO_NDK_VERSION: "3.5.4" 14 | 15 | jobs: 16 | build-native-libs: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | include: 22 | - android-abi: arm64-v8a 23 | rust-target: aarch64-linux-android 24 | 25 | - android-abi: armeabi-v7a 26 | rust-target: armv7-linux-androideabi 27 | 28 | - android-abi: x86_64 29 | rust-target: x86_64-linux-android 30 | 31 | - android-abi: x86 32 | rust-target: i686-linux-android 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: Install Rust toolchain 38 | run: | 39 | rustup show 40 | rustup target add ${{ matrix.rust-target }} 41 | 42 | - name: Cache Cargo output 43 | uses: Swatinem/rust-cache@v2 44 | with: 45 | shared-key: ${{ matrix.rust-target }} 46 | save-if: ${{ github.ref == 'refs/heads/main' }} 47 | 48 | - name: Install cargo-ndk 49 | run: cargo install cargo-ndk@${{ env.CARGO_NDK_VERSION }} --locked 50 | 51 | - name: Set up NDK 52 | uses: nttld/setup-ndk@v1 53 | id: setup-ndk 54 | with: 55 | ndk-version: ${{ env.NDK_VERSION }} 56 | link-to-sdk: true 57 | 58 | - name: Build native libs 59 | run: | 60 | unset ANDROID_SDK_ROOT # Deprecated, will cause an error if left set. 61 | cargo ndk --bindgen --target ${{ matrix.android-abi }} --platform 26 -o jniLibs build --release --features jpegxr 62 | env: 63 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 64 | 65 | - uses: actions/upload-artifact@v4 66 | with: 67 | name: native-lib-${{ matrix.android-abi }} 68 | path: jniLibs 69 | 70 | build-apks: 71 | needs: build-native-libs 72 | runs-on: ubuntu-latest 73 | env: 74 | KEYSTORE: ${{ secrets.KEYSTORE }} 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - uses: actions/download-artifact@v4 80 | with: # no name set, so all artifacts are downloaded 81 | path: native-libs 82 | 83 | - name: Copy native libs 84 | run: | 85 | mkdir app/src/main/jniLibs 86 | cp -r native-libs/*/* app/src/main/jniLibs/ 87 | 88 | - name: Set up Java 17 89 | uses: actions/setup-java@v4 90 | with: 91 | distribution: 'temurin' 92 | java-version: '17' 93 | 94 | - name: Setup Gradle 95 | uses: gradle/actions/setup-gradle@v4 96 | 97 | - name: Decode keystore 98 | if: ${{ env.KEYSTORE != '' }} 99 | run: echo $KEYSTORE | base64 -di > app/androidkey.jks 100 | 101 | - name: Generate dummy keystore 102 | if: ${{ env.KEYSTORE == '' }} 103 | run: | 104 | keytool -genkeypair -v -keystore app/androidkey.jks -alias dummy_alias \ 105 | -storepass dummy_pass -keypass dummy_pass -keyalg RSA -keysize 2048 -validity 10000 \ 106 | -dname "CN=example, OU=example, O=example, L=example, S=example, C=example" 107 | echo 'SIGNING_KEY_ALIAS=dummy_alias' >> $GITHUB_ENV 108 | echo 'SIGNING_STORE_PASSWORD=dummy_pass' >> $GITHUB_ENV 109 | echo 'SIGNING_KEY_PASSWORD=dummy_pass' >> $GITHUB_ENV 110 | 111 | - name: Build release APK 112 | env: 113 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS || env.SIGNING_KEY_ALIAS }} 114 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD || env.SIGNING_KEY_PASSWORD }} 115 | SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD || env.SIGNING_STORE_PASSWORD }} 116 | run: ./gradlew assembleRelease 117 | 118 | - uses: actions/upload-artifact@v4 119 | with: 120 | name: ruffle-release-apks 121 | path: app/build/outputs/apk/release/*.apk 122 | 123 | android-tests: 124 | name: Android Tests 125 | needs: build-native-libs 126 | runs-on: ubuntu-latest 127 | strategy: 128 | matrix: 129 | api-level: [26, 35] 130 | 131 | steps: 132 | - uses: actions/checkout@v4 133 | 134 | - uses: actions/download-artifact@v4 135 | with: # no name set, so all artifacts are downloaded 136 | path: native-libs 137 | 138 | - name: Copy native libs 139 | run: | 140 | mkdir app/src/main/jniLibs 141 | cp -r native-libs/*/* app/src/main/jniLibs/ 142 | 143 | - name: Set up Java 17 144 | uses: actions/setup-java@v4 145 | with: 146 | distribution: 'temurin' 147 | java-version: '17' 148 | 149 | - name: Setup Gradle 150 | uses: gradle/actions/setup-gradle@v4 151 | 152 | # https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ 153 | - name: Enable KVM 154 | run: | 155 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 156 | sudo udevadm control --reload-rules 157 | sudo udevadm trigger --name-match=kvm 158 | 159 | - name: Test 160 | uses: reactivecircus/android-emulator-runner@v2 161 | with: 162 | api-level: ${{ matrix.api-level }} 163 | arch: x86_64 164 | script: | 165 | adb shell settings put secure immersive_mode_confirmations confirmed 166 | ./gradlew connectedCheck 167 | 168 | - name: Upload Test Report 169 | uses: actions/upload-artifact@v4 170 | if: ${{ !cancelled() }} # always run even if the previous step fails 171 | with: 172 | name: junit-test-results-${{ matrix.api-level }} 173 | path: '**/build/outputs/**/TEST-*.xml' 174 | retention-days: 1 175 | 176 | -------------------------------------------------------------------------------- /.github/workflows/fastlane.yml: -------------------------------------------------------------------------------- 1 | name: Validate Fastlane metadata 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | go: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Validate Fastlane Supply Metadata 14 | uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Format 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | rust: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install Rust toolchain 22 | run: | 23 | rustup show 24 | rustup target add aarch64-linux-android 25 | rustup component add rustfmt clippy 26 | 27 | - name: Cache Cargo output 28 | uses: Swatinem/rust-cache@v2 29 | with: 30 | shared-key: clippy 31 | save-if: ${{ github.ref == 'refs/heads/main' }} 32 | 33 | - name: Check formatting 34 | run: cargo fmt --all -- --check 35 | 36 | - name: Install cargo-ndk 37 | run: cargo install cargo-ndk 38 | 39 | - name: Check clippy 40 | run: cargo ndk --bindgen -t arm64-v8a -- clippy --all --all-features --tests -- -D warnings 41 | 42 | android: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Set up JDK 17 49 | uses: actions/setup-java@v4.2.1 50 | with: 51 | distribution: 'temurin' 52 | java-version: '17' 53 | 54 | - uses: gradle/actions/wrapper-validation@v4 55 | name: Validate Gradle Wrapper 56 | 57 | - name: Check ktlint 58 | run: ./gradlew ktlintCheck 59 | 60 | - name: Check lint 61 | run: ./gradlew lint 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Make a Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | make-release: 8 | name: Make Release 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Download artifacts 15 | id: download-artifacts 16 | uses: dawidd6/action-download-artifact@v10 17 | with: 18 | workflow: build.yml 19 | branch: main 20 | workflow_conclusion: success 21 | name: ruffle-release-apks 22 | 23 | - name: Get current date 24 | uses: 1466587594/get-current-time@v2.1.2 25 | id: current_date 26 | with: 27 | format: YYYYMMDD 28 | 29 | - name: Create release 30 | id: create_release 31 | run: | 32 | tag_name="${{ steps.current_date.outputs.formattedTime }}" 33 | release_name="${{ steps.current_date.outputs.formattedTime }}" 34 | gh release create "$tag_name" --title "$release_name" --generate-notes --prerelease 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Upload APKs 39 | run: gh release upload "${{ steps.current_date.outputs.formattedTime }}" *.apk 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/report_test_results.yml: -------------------------------------------------------------------------------- 1 | name: Report test results 2 | on: 3 | workflow_run: 4 | workflows: [build] 5 | types: [completed] 6 | 7 | permissions: 8 | checks: write 9 | 10 | jobs: 11 | checks: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Download Test Report 15 | uses: dawidd6/action-download-artifact@v10 16 | with: 17 | name: junit-test-results-.* 18 | name_is_regexp: true 19 | workflow: ${{ github.event.workflow.id }} 20 | run_id: ${{ github.event.workflow_run.id }} 21 | - name: Publish Test Report 22 | uses: mikepenz/action-junit-report@v5 23 | with: 24 | commit: ${{github.event.workflow_run.head_sha}} 25 | report_paths: '**/TEST-*.xml' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /target 12 | *.jks 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ruffle Android 2 | 🎉 Thanks for your interest in Ruffle! Contributions of all kinds are welcome. 3 | 4 | 5 | * [Contributing to Ruffle Android](#contributing-to-ruffle-android) 6 | * [Building from source](#building-from-source) 7 | * [Requirements](#requirements) 8 | * [Building from Command Line](#building-from-command-line) 9 | * [Building from Android Studio](#building-from-android-studio) 10 | * [Development tips](#development-tips) 11 | * [Limit your targets](#limit-your-targets) 12 | * [Use emulators!](#use-emulators) 13 | * [Troubleshooting](#troubleshooting) 14 | * ["error: Error detecting NDK version for path"](#error-error-detecting-ndk-version-for-path) 15 | * [Code guidelines](#code-guidelines) 16 | * [Rust](#rust) 17 | * [Kotlin](#kotlin) 18 | 19 | 20 | ## Building from source 21 | ### Requirements 22 | Before you can build the app from source, you'll need to grab a few things. 23 | 24 | - Install Android Studio with at least the Platform SDK (e.g. version 35) and the NDK Tools (e.g. version 26). 25 | - Install jdk 17 (potentially included with Android Studio) 26 | - Install [rust](https://rustup.rs/) 27 | - `cargo install cargo-ndk` 28 | - `rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android` 29 | - Set the `ANDROID_NDK_ROOT` environment variable to the location of the versioned ndk folder - for example `ANDROID_NDK_ROOT=$HOME/Android/Sdk/ndk/24.0.8215888/` 30 | 31 | ### Building from Command Line 32 | If you're completely using the command line and bypassing Android Studio, make sure that the `local.properties` 33 | file exists at the repository root folder. It can be empty. 34 | 35 | To build the apks run `./gradlew assembleDebug`. You will find the resulting APKs in `app/build/outputs/apk/debug/`. 36 | 37 | ### Building from Android Studio 38 | Just open the repository root directory in Android Studio, it should automatically set up all the right things. 39 | 40 | ### Development tips 41 | #### Limit your targets 42 | To speed up iteration, you can tell gradle to only build the rust project for one specific target - the emulator/device you're using. 43 | To do this, add `ndkTargets=arm64` (for example) to your `local.properties`. To specify more than one target, separate them with a space. 44 | The default value is all 4 targets, so that's cutting the build time by 75%! 45 | 46 | #### Use emulators! 47 | An Android device is **not** required to build or test Ruffle. Android Studio defaults to deploying and testing on an emulator. 48 | 49 | Feel free to install other emulators to help test different form factors or older versions of Android OS. 50 | 51 | ### Use android target 52 | Set the target of your favourite tools (such as Rust Rover or Rust Analyzer) to `aarch64-linux-android` (or similar) 53 | to stop any errors from it trying to compile Android code for an unsupported platform. 54 | If that isn't enough, you may also need to set the `TARGET_CC`, `TARGET_CXX`, and `TARGET_AR` environment variables to the 55 | full paths of the (sometimes API level-specific) `clang`, `clang++`, and `llvm-ar` binaries from the NDK, respectively. 56 | 57 | ### Troubleshooting 58 | 59 | #### "error: Error detecting NDK version for path" 60 | This means your `ANDROID_NDK_ROOT` environment variable is missing or incorrect. 61 | 62 | ## Code guidelines 63 | We have strict guidelines about trivial code quality matters (formatting and easily checkable lints). 64 | These are enforced by Github Actions, but you are encouraged to run these locally to avoid having to iterate *after* a PR has been opened. 65 | 66 | ### Rust 67 | Ruffle is built using the latest stable version of the Rust compiler. Nightly and unstable features should be avoided. 68 | The Rust code in Ruffle strives to be idiomatic. The Rust compiler should emit no warnings when building the project. 69 | Additionally, all code should be formatted using [`rustfmt`](https://github.com/rust-lang/rustfmt) and linted using [`clippy`](https://github.com/rust-lang/rust-clippy). 70 | You can install these tools using `rustup`: 71 | 72 | ```sh 73 | rustup component add rustfmt 74 | rustup component add clippy 75 | ``` 76 | 77 | You can auto-format your changes with `rustfmt` 78 | 79 | ```sh 80 | cargo fmt --all 81 | ``` 82 | 83 | And you can run the clippy lints: 84 | 85 | ```sh 86 | cargo ndk clippy --all --tests 87 | ``` 88 | 89 | Specific warnings and clippy lints can be allowed when appropriate using attributes, such as: 90 | 91 | ```rs 92 | #[allow(clippy::float_cmp)] 93 | ``` 94 | 95 | ### Kotlin 96 | We try to use Kotlin where possible, no Java. Additionally, we try to use best practices and maintain readable, idiomatic code. 97 | 98 | To automatically format all Kotlin code, run: 99 | 100 | ```sh 101 | ./gradlew ktlintFormat 102 | ``` 103 | 104 | And to automatically lint the code, use: 105 | 106 | ```sh 107 | ./gradlew lint 108 | ``` 109 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | # Underscore because the cargo-ndk gradle plugin doesn't transform a hyphen to it. 3 | name = "ruffle_android" 4 | version = "0.1.0" 5 | authors = ["TÖRÖK Attila "] 6 | edition = "2018" 7 | resolver = "2" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [profile.release] 14 | strip = "symbols" 15 | 16 | [dependencies] 17 | 18 | android-activity = { version = "0.6.0", features = ["game-activity"] } 19 | android_logger = "0.15.0" 20 | 21 | jni = "0.21.1" 22 | ndk = { version = "0.9.0", features = ["audio"] } 23 | ndk-context = "0.1.1" 24 | 25 | # Have to follow Ruffle with this. 26 | wgpu = "24.0.5" 27 | 28 | ruffle_core = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master", features = [ 29 | "audio", 30 | "symphonia", 31 | "mp3", 32 | "nellymoser", 33 | "lzma", 34 | "default_compatibility_rules", 35 | "default_font", 36 | ] } 37 | 38 | ruffle_render_wgpu = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master" } 39 | ruffle_video_software = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master" } 40 | ruffle_frontend_utils = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master" } 41 | 42 | log = "0.4.27" 43 | 44 | # Redirect tracing to log 45 | tracing = {version = "0.1.41", features = ["log", "log-always"]} 46 | backtrace = "0.3.75" 47 | 48 | url = "2.5.2" 49 | webbrowser = "1.0.4" 50 | 51 | tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros"]} 52 | 53 | [features] 54 | jpegxr = ["ruffle_core/jpegxr"] 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Ruffle 2 | 3 | Ruffle is licensed under either of 4 | 5 | - Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 6 | - MIT license (http://opensource.org/licenses/MIT) 7 | 8 | at your option. 9 | 10 | ## MIT License 11 | 12 | Copyright (c) 2024 to Ruffle and Ruffle Contributors 13 | (https://github.com/ruffle-rs/ruffle-android/graphs/contributors) 14 | 15 | Permission is hereby granted, free of charge, to any 16 | person obtaining a copy of this software and associated 17 | documentation files (the "Software"), to deal in the 18 | Software without restriction, including without 19 | limitation the rights to use, copy, modify, merge, 20 | publish, distribute, sublicense, and/or sell copies of 21 | the Software, and to permit persons to whom the Software 22 | is furnished to do so, subject to the following 23 | conditions: 24 | 25 | The above copyright notice and this permission notice 26 | shall be included in all copies or substantial portions 27 | of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 30 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 31 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 32 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 33 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 34 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 35 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 36 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 37 | DEALINGS IN THE SOFTWARE. 38 | 39 | ## Apache License, Version 2.0 40 | 41 | Apache License 42 | Version 2.0, January 2004 43 | http://www.apache.org/licenses/ 44 | 45 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 46 | 47 | 1. Definitions. 48 | 49 | "License" shall mean the terms and conditions for use, reproduction, 50 | and distribution as defined by Sections 1 through 9 of this document. 51 | 52 | "Licensor" shall mean the copyright owner or entity authorized by 53 | the copyright owner that is granting the License. 54 | 55 | "Legal Entity" shall mean the union of the acting entity and all 56 | other entities that control, are controlled by, or are under common 57 | control with that entity. For the purposes of this definition, 58 | "control" means (i) the power, direct or indirect, to cause the 59 | direction or management of such entity, whether by contract or 60 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 61 | outstanding shares, or (iii) beneficial ownership of such entity. 62 | 63 | "You" (or "Your") shall mean an individual or Legal Entity 64 | exercising permissions granted by this License. 65 | 66 | "Source" form shall mean the preferred form for making modifications, 67 | including but not limited to software source code, documentation 68 | source, and configuration files. 69 | 70 | "Object" form shall mean any form resulting from mechanical 71 | transformation or translation of a Source form, including but 72 | not limited to compiled object code, generated documentation, 73 | and conversions to other media types. 74 | 75 | "Work" shall mean the work of authorship, whether in Source or 76 | Object form, made available under the License, as indicated by a 77 | copyright notice that is included in or attached to the work 78 | (an example is provided in the Appendix below). 79 | 80 | "Derivative Works" shall mean any work, whether in Source or Object 81 | form, that is based on (or derived from) the Work and for which the 82 | editorial revisions, annotations, elaborations, or other modifications 83 | represent, as a whole, an original work of authorship. For the purposes 84 | of this License, Derivative Works shall not include works that remain 85 | separable from, or merely link (or bind by name) to the interfaces of, 86 | the Work and Derivative Works thereof. 87 | 88 | "Contribution" shall mean any work of authorship, including 89 | the original version of the Work and any modifications or additions 90 | to that Work or Derivative Works thereof, that is intentionally 91 | submitted to Licensor for inclusion in the Work by the copyright owner 92 | or by an individual or Legal Entity authorized to submit on behalf of 93 | the copyright owner. For the purposes of this definition, "submitted" 94 | means any form of electronic, verbal, or written communication sent 95 | to the Licensor or its representatives, including but not limited to 96 | communication on electronic mailing lists, source code control systems, 97 | and issue tracking systems that are managed by, or on behalf of, the 98 | Licensor for the purpose of discussing and improving the Work, but 99 | excluding communication that is conspicuously marked or otherwise 100 | designated in writing by the copyright owner as "Not a Contribution." 101 | 102 | "Contributor" shall mean Licensor and any individual or Legal Entity 103 | on behalf of whom a Contribution has been received by Licensor and 104 | subsequently incorporated within the Work. 105 | 106 | 2. Grant of Copyright License. Subject to the terms and conditions of 107 | this License, each Contributor hereby grants to You a perpetual, 108 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 109 | copyright license to reproduce, prepare Derivative Works of, 110 | publicly display, publicly perform, sublicense, and distribute the 111 | Work and such Derivative Works in Source or Object form. 112 | 113 | 3. Grant of Patent License. Subject to the terms and conditions of 114 | this License, each Contributor hereby grants to You a perpetual, 115 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 116 | (except as stated in this section) patent license to make, have made, 117 | use, offer to sell, sell, import, and otherwise transfer the Work, 118 | where such license applies only to those patent claims licensable 119 | by such Contributor that are necessarily infringed by their 120 | Contribution(s) alone or by combination of their Contribution(s) 121 | with the Work to which such Contribution(s) was submitted. If You 122 | institute patent litigation against any entity (including a 123 | cross-claim or counterclaim in a lawsuit) alleging that the Work 124 | or a Contribution incorporated within the Work constitutes direct 125 | or contributory patent infringement, then any patent licenses 126 | granted to You under this License for that Work shall terminate 127 | as of the date such litigation is filed. 128 | 129 | 4. Redistribution. You may reproduce and distribute copies of the 130 | Work or Derivative Works thereof in any medium, with or without 131 | modifications, and in Source or Object form, provided that You 132 | meet the following conditions: 133 | 134 | (a) You must give any other recipients of the Work or 135 | Derivative Works a copy of this License; and 136 | 137 | (b) You must cause any modified files to carry prominent notices 138 | stating that You changed the files; and 139 | 140 | (c) You must retain, in the Source form of any Derivative Works 141 | that You distribute, all copyright, patent, trademark, and 142 | attribution notices from the Source form of the Work, 143 | excluding those notices that do not pertain to any part of 144 | the Derivative Works; and 145 | 146 | (d) If the Work includes a "NOTICE" text file as part of its 147 | distribution, then any Derivative Works that You distribute must 148 | include a readable copy of the attribution notices contained 149 | within such NOTICE file, excluding those notices that do not 150 | pertain to any part of the Derivative Works, in at least one 151 | of the following places: within a NOTICE text file distributed 152 | as part of the Derivative Works; within the Source form or 153 | documentation, if provided along with the Derivative Works; or, 154 | within a display generated by the Derivative Works, if and 155 | wherever such third-party notices normally appear. The contents 156 | of the NOTICE file are for informational purposes only and 157 | do not modify the License. You may add Your own attribution 158 | notices within Derivative Works that You distribute, alongside 159 | or as an addendum to the NOTICE text from the Work, provided 160 | that such additional attribution notices cannot be construed 161 | as modifying the License. 162 | 163 | You may add Your own copyright statement to Your modifications and 164 | may provide additional or different license terms and conditions 165 | for use, reproduction, or distribution of Your modifications, or 166 | for any such Derivative Works as a whole, provided Your use, 167 | reproduction, and distribution of the Work otherwise complies with 168 | the conditions stated in this License. 169 | 170 | 5. Submission of Contributions. Unless You explicitly state otherwise, 171 | any Contribution intentionally submitted for inclusion in the Work 172 | by You to the Licensor shall be under the terms and conditions of 173 | this License, without any additional terms or conditions. 174 | Notwithstanding the above, nothing herein shall supersede or modify 175 | the terms of any separate license agreement you may have executed 176 | with Licensor regarding such Contributions. 177 | 178 | 6. Trademarks. This License does not grant permission to use the trade 179 | names, trademarks, service marks, or product names of the Licensor, 180 | except as required for reasonable and customary use in describing the 181 | origin of the Work and reproducing the content of the NOTICE file. 182 | 183 | 7. Disclaimer of Warranty. Unless required by applicable law or 184 | agreed to in writing, Licensor provides the Work (and each 185 | Contributor provides its Contributions) on an "AS IS" BASIS, 186 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 187 | implied, including, without limitation, any warranties or conditions 188 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 189 | PARTICULAR PURPOSE. You are solely responsible for determining the 190 | appropriateness of using or redistributing the Work and assume any 191 | risks associated with Your exercise of permissions under this License. 192 | 193 | 8. Limitation of Liability. In no event and under no legal theory, 194 | whether in tort (including negligence), contract, or otherwise, 195 | unless required by applicable law (such as deliberate and grossly 196 | negligent acts) or agreed to in writing, shall any Contributor be 197 | liable to You for damages, including any direct, indirect, special, 198 | incidental, or consequential damages of any character arising as a 199 | result of this License or out of the use or inability to use the 200 | Work (including but not limited to damages for loss of goodwill, 201 | work stoppage, computer failure or malfunction, or any and all 202 | other commercial damages or losses), even if such Contributor 203 | has been advised of the possibility of such damages. 204 | 205 | 9. Accepting Warranty or Additional Liability. While redistributing 206 | the Work or Derivative Works thereof, You may choose to offer, 207 | and charge a fee for, acceptance of support, warranty, indemnity, 208 | or other liability obligations and/or rights consistent with this 209 | License. However, in accepting such obligations, You may act only 210 | on Your own behalf and on Your sole responsibility, not on behalf 211 | of any other Contributor, and only if You agree to indemnify, 212 | defend, and hold each Contributor harmless for any liability 213 | incurred by, or claims asserted against, such Contributor by reason 214 | of your accepting any such warranty or additional liability. 215 | 216 | END OF TERMS AND CONDITIONS 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a native Android application for [Ruffle](https://ruffle.rs). 2 | 3 | It is in a very early stage. 4 | 5 | # Prebuilt APKs 6 | 7 | The latest release [(here)](https://github.com/torokati44/ruffle-android/releases) should have a few `.apk` files uploaded as assets. 8 | 9 | You can try this app by downloading and installing one of those. 10 | 11 | - **For the vast majority of modern phones, tablets, single board computers, and small game consoles, you'll need the `arm64-v8a` version.** 12 | 13 | - The `armeabi-v7a` version is for older, 32-bit ARM stuff. 14 | 15 | - The `x86_64` version is for some rare Intel/Microsoft tablets and/or for Chromebooks, and/or for running on a PC on Android-x86 or in Waydroid or similar. 16 | 17 | - The `x86` version is there mostly just for completeness. 18 | 19 | - The `universal` version should work on all 4 of the above architectures, but it's _huge_. 20 | 21 | # Building from source 22 | 23 | Please see [CONTRIBUTING.md](CONTRIBUTING.md#building-from-source) for details about how to build this repository yourself. 24 | 25 | --- 26 | 27 | # TODO 28 | 29 | In no particular order: 30 | 31 | - [ ] Ability to show the built-in virtual keyboard (softinput), for text input 32 | - [ ] Controller/Gamepad input? 33 | - Mapped to key presses and/or virtual mouse pointer 34 | - [ ] Own custom keyboard overlay, maybe even per-content configs 35 | - Not an overlay, and not per-content, but custom keyboard is there 36 | - [ ] Error/panic handling 37 | - [ ] Loading "animation" (spinner) 38 | - [ ] Alternative audio backend (OpenSL ES) for Android < 8 39 | - [ ] Proper storage backend? 40 | - [ ] Resolve design glitches/styling/theming (immersive mode, window insets for holes/notches/corners) 41 | - [ ] Publish to various app stores, maybe automatically? 42 | - [ ] Bundle demo animations/games 43 | - [ ] Add ability to load content from well known online collections? (well maybe not z0r... unless?) 44 | - [ ] History, favorites, other flair...? 45 | 46 | ### DONE: 47 | 48 | - [X] Clean up ~everything 49 | - [X] Cross-platform build instructions? 50 | - I think gradle should take care of it now 51 | - [X] UI backend (context menu) 52 | - Context menu works 53 | - [X] Logging? 54 | - [X] Navigator backend (fetch, open browser) 55 | - Opening links works at least 56 | - [X] Touch/mouse input 57 | - [X] Keyboard input: only with physical keyboard connected or through `scrcpy` 58 | - This was needed: https://github.com/rust-windowing/winit/pull/2226 59 | - [X] Split into a separate repo 60 | - [X] Add ability to Open SWF by entered/pasted URL (or even directly from clipboard) 61 | - No direct clipboard open, but easy to paste into the text field... 62 | - [X] Unglitchify rendering: scale, center and letterbox the content properly 63 | - [ ] Ask CPAL/Oboe to open a "media" type output stream instead of a "call" one 64 | - so the right volume slider controls it, and it uses the loud(er)speaker 65 | - -> solved by switching to a direct AAudio (ndk-audio) backend 66 | - [X] Add building this to CI, at least to the release workflow 67 | - This repo has its own CI setup, which builds APKs 68 | - [X] Simplify build process (hook cargo-apk into gradle, drop cargo-apk?) 69 | - ~cargo-apk is fine, but is only used to detect the SDK/NDK environment and run Cargo in it, and not to build an APK.~ 70 | - actually solved by switching to `cargo-ndk` and the corresponding Gradle plugin 71 | - [X] Somehow filter files to be picked to .swf 72 | - How well this works depends on the file picker, but it "should work most of the time" 73 | - [X] Unglitchify audio volume (buttons unresponsive?) 74 | - (pending: https://github.com/rust-windowing/winit/pull/1919) 75 | - actually solved by switching to GameActivity instead 76 | - [ ] Register Ruffle to open .swf files 77 | - How well this works depends on the application opening the file, but it "should work most of the time" 78 | - [X] Figure out why videos are not playing (could be a seeking issue) 79 | - The video decoder features weren't enabled on `ruffle_core`... 80 | - [X] Sign the APK 81 | - Using a very simple key for now, with just my name in it 82 | - [X] Support for 32-bit ARM phones 83 | - Untested, but should work in theory 84 | - [X] Support for x86(_64) tablets? 85 | - Sorted out 86 | - [X] Consider not building the intermediate .apk just for the shared libraries 87 | - Figured out, no intermediate .apk any more, only native libs built 88 | - [ ] Unbreak the regular build on CI 89 | - No longer relevant after the repo split 90 | - [ ] Clean up commit history of the branch 91 | - No longer relevant after the repo split 92 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/main/jniLibs 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties 4 | import com.github.willir.rust.CargoNdkBuildTask 5 | import org.jetbrains.kotlin.konan.properties.hasProperty 6 | import org.jetbrains.kotlin.konan.properties.propertyList 7 | 8 | plugins { 9 | alias(libs.plugins.androidApplication) 10 | alias(libs.plugins.jetbrainsKotlinAndroid) 11 | alias(libs.plugins.cargoNdkAndroid) 12 | alias(libs.plugins.composeCompiler) 13 | } 14 | 15 | android { 16 | namespace = "rs.ruffle" 17 | compileSdk = 35 18 | 19 | defaultConfig { 20 | applicationId = "rs.ruffle" 21 | minSdk = 26 22 | targetSdk = 35 23 | versionCode = 1 24 | versionName = "1.0" 25 | 26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 27 | vectorDrawables { 28 | useSupportLibrary = true 29 | } 30 | 31 | ndk { 32 | abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")) 33 | } 34 | } 35 | 36 | signingConfigs { 37 | val keyFile = file("androidkey.jks") 38 | val storePasswordVal = System.getenv("SIGNING_STORE_PASSWORD") 39 | if (keyFile.exists() && storePasswordVal.isNotEmpty()) { 40 | create("release") { 41 | storeFile = keyFile 42 | storePassword = storePasswordVal 43 | keyAlias = System.getenv("SIGNING_KEY_ALIAS") 44 | keyPassword = System.getenv("SIGNING_KEY_PASSWORD") 45 | } 46 | } 47 | } 48 | 49 | buildTypes { 50 | release { 51 | isMinifyEnabled = false 52 | proguardFiles( 53 | getDefaultProguardFile("proguard-android-optimize.txt"), 54 | "proguard-rules.pro" 55 | ) 56 | signingConfig = signingConfigs.findByName("release") 57 | } 58 | } 59 | 60 | compileOptions { 61 | sourceCompatibility = JavaVersion.VERSION_1_8 62 | targetCompatibility = JavaVersion.VERSION_1_8 63 | } 64 | 65 | kotlinOptions { 66 | jvmTarget = "1.8" 67 | } 68 | 69 | buildFeatures { 70 | compose = true 71 | prefab = true 72 | } 73 | 74 | packaging { 75 | resources { 76 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 77 | } 78 | } 79 | 80 | splits { 81 | // Configures multiple APKs based on ABI. 82 | abi { 83 | // Enables building multiple APKs per ABI. 84 | isEnable = true 85 | 86 | // Resets the list of ABIs that Gradle should create APKs for to none. 87 | reset() 88 | 89 | // Specifies a list of ABIs that Gradle should create APKs for. 90 | include("arm64-v8a", "armeabi-v7a", "x86_64", "x86") 91 | 92 | // Specifies that we also want to generate a universal APK that includes all ABIs. 93 | isUniversalApk = true 94 | } 95 | } 96 | } 97 | 98 | dependencies { 99 | 100 | implementation(libs.androidx.core.ktx) 101 | implementation(libs.androidx.lifecycle.runtime.ktx) 102 | implementation(libs.androidx.activity.compose) 103 | implementation(platform(libs.androidx.compose.bom)) 104 | implementation(libs.androidx.ui) 105 | implementation(libs.androidx.ui.graphics) 106 | implementation(libs.androidx.ui.tooling.preview) 107 | implementation(libs.androidx.material3) 108 | implementation(libs.androidx.lifecycle.viewmodel.compose) 109 | implementation(libs.androidx.navigation.runtime.ktx) 110 | implementation(libs.androidx.navigation.compose) 111 | implementation(libs.androidx.games.activity) 112 | implementation(libs.androidx.constraintlayout) 113 | implementation(libs.androidx.appcompat) 114 | androidTestImplementation(libs.androidx.uiautomator) 115 | androidTestImplementation(libs.androidx.test.runner) 116 | androidTestImplementation(libs.androidx.test.rules) 117 | testImplementation(libs.junit) 118 | androidTestImplementation(libs.androidx.junit) 119 | androidTestImplementation(libs.androidx.espresso.core) 120 | androidTestImplementation(platform(libs.androidx.compose.bom)) 121 | androidTestImplementation(libs.androidx.ui.test.junit4) 122 | debugImplementation(libs.androidx.ui.tooling) 123 | debugImplementation(libs.androidx.ui.test.manifest) 124 | } 125 | 126 | // On GHA, we prebuild the native libs separately for fasterness, 127 | // and this plugin doesn't recognize them, so would build them again. 128 | if (System.getenv("GITHUB_ACTIONS") != null) { 129 | tasks.withType { 130 | enabled = false 131 | } 132 | } 133 | 134 | cargoNdk { 135 | module = "." 136 | apiLevel = 26 137 | buildType = "release" 138 | 139 | val localProperties = gradleLocalProperties(rootDir, providers) 140 | 141 | if (localProperties.hasProperty("ndkTargets")) { 142 | targets = ArrayList(localProperties.propertyList("ndkTargets")) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/androidTest/java/rs/ruffle/InputEvents.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Point 7 | import android.graphics.Rect 8 | import android.net.Uri 9 | import android.os.SystemClock 10 | import android.view.KeyEvent 11 | import androidx.test.core.app.ApplicationProvider 12 | import androidx.test.espresso.matcher.ViewMatchers 13 | import androidx.test.ext.junit.runners.AndroidJUnit4 14 | import androidx.test.platform.app.InstrumentationRegistry 15 | import androidx.test.uiautomator.By 16 | import androidx.test.uiautomator.UiDevice 17 | import androidx.test.uiautomator.Until 18 | import java.io.File 19 | import java.util.concurrent.TimeoutException 20 | import kotlin.math.min 21 | import kotlin.math.roundToInt 22 | import org.hamcrest.CoreMatchers 23 | import org.junit.Before 24 | import org.junit.Test 25 | import org.junit.runner.RunWith 26 | 27 | private const val BASIC_SAMPLE_PACKAGE = "rs.ruffle" 28 | private const val LAUNCH_TIMEOUT = 5000L 29 | private const val SWF_WIDTH = 550.0 30 | private const val SWF_HEIGHT = 400.0 31 | 32 | @RunWith(AndroidJUnit4::class) 33 | class InputEvents { 34 | private lateinit var device: UiDevice 35 | private lateinit var traceOutput: File 36 | private var lastTraceSize: Long = 0 37 | private lateinit var swfFile: File 38 | 39 | @Before 40 | fun startMainActivityFromHomeScreen() { 41 | // Initialize UiDevice instance 42 | device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 43 | 44 | // Start from the home screen 45 | device.pressHome() 46 | 47 | // Wait for launcher 48 | val launcherPackage: String = device.launcherPackageName 49 | ViewMatchers.assertThat(launcherPackage, CoreMatchers.notNullValue()) 50 | device.wait( 51 | Until.hasObject(By.pkg(launcherPackage).depth(0)), 52 | LAUNCH_TIMEOUT 53 | ) 54 | 55 | // Launch the app 56 | val context = ApplicationProvider.getApplicationContext() 57 | traceOutput = File.createTempFile("trace", ".txt", context.cacheDir) 58 | swfFile = File.createTempFile("movie", ".swf", context.cacheDir) 59 | lastTraceSize = 0 60 | val resources = InstrumentationRegistry.getInstrumentation().context.resources 61 | val inStream = resources.openRawResource( 62 | rs.ruffle.test.R.raw.input_test 63 | ) 64 | val bytes = inStream.readBytes() 65 | swfFile.writeBytes(bytes) 66 | val intent = context.packageManager.getLaunchIntentForPackage( 67 | BASIC_SAMPLE_PACKAGE 68 | )?.apply { 69 | component = ComponentName("rs.ruffle", "rs.ruffle.PlayerActivity") 70 | data = Uri.fromFile(swfFile) 71 | putExtra("traceOutput", traceOutput.absolutePath) 72 | // Clear out any previous instances 73 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 74 | } 75 | context.startActivity(intent) 76 | 77 | // Wait for the app to appear 78 | device.wait( 79 | Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), 80 | LAUNCH_TIMEOUT 81 | ) 82 | } 83 | 84 | @Test 85 | fun clickEvents() { 86 | waitUntilNewLogAndIdle() 87 | 88 | val player = device.findObject(By.desc("Ruffle Player")) 89 | 90 | val red = screenToSwf(player.visibleBounds, Point(50, 50)) 91 | val blue = screenToSwf(player.visibleBounds, Point(500, 350)) 92 | device.click(red) 93 | device.click(blue) 94 | device.drag(red.x, red.y, blue.x, blue.y, 100) 95 | waitUntilNewLogAndIdle() 96 | 97 | val trace = traceOutput.readLines() 98 | ViewMatchers.assertThat( 99 | trace, 100 | CoreMatchers.equalTo( 101 | listOf( 102 | "Test started!", 103 | "red received mouseDown", 104 | "red received mouseUp", 105 | "red received click", 106 | "blue received mouseDown", 107 | "blue received mouseUp", 108 | "blue received click", 109 | "red received mouseDown", 110 | "blue received mouseUp" 111 | ) 112 | ) 113 | ) 114 | } 115 | 116 | @Test 117 | fun keyEvents() { 118 | waitUntilNewLogAndIdle() 119 | 120 | device.pressKeyCode(KeyEvent.KEYCODE_A) 121 | device.pressKeyCode(KeyEvent.KEYCODE_B) 122 | 123 | waitUntilNewLogAndIdle() 124 | 125 | val trace = traceOutput.readLines() 126 | ViewMatchers.assertThat( 127 | trace, 128 | CoreMatchers.equalTo( 129 | listOf( 130 | "Test started!", 131 | "keyDown: keyCode = 65, charCode = 97", 132 | "keyUp: keyCode = 65, charCode = 97", 133 | "keyDown: keyCode = 66, charCode = 98", 134 | "keyUp: keyCode = 66, charCode = 98" 135 | ) 136 | ) 137 | ) 138 | } 139 | 140 | private fun screenToSwf(playerBounds: Rect, point: Point): Point { 141 | val stretchX = playerBounds.width() / SWF_WIDTH 142 | val stretchY = playerBounds.height() / SWF_HEIGHT 143 | val scaleFactor = min(stretchX, stretchY) 144 | val swfScreenWidth = SWF_WIDTH * scaleFactor 145 | val swfScreenHeight = SWF_HEIGHT * scaleFactor 146 | val swfOffsetX = (playerBounds.width() - swfScreenWidth) / 2 147 | val swfOffsetY = (playerBounds.height() - swfScreenHeight) / 2 148 | return Point( 149 | (playerBounds.left + swfOffsetX + point.x * scaleFactor).roundToInt(), 150 | (playerBounds.top + swfOffsetY + point.y * scaleFactor).roundToInt() 151 | ) 152 | } 153 | 154 | /** 155 | * Waits until the log file receives new trace output, and then waits for it to become idle with 156 | * no more output for a period of time. 157 | * 158 | * @param idleWindowMillis How long the log file must stay the same size for, before it's considered idle 159 | * @param timeoutMillis How long to keep waiting for, before throwing an error 160 | */ 161 | private fun waitUntilNewLogAndIdle(idleWindowMillis: Long = 1000, timeoutMillis: Long = 10000) { 162 | val startTimeMillis = SystemClock.uptimeMillis() 163 | val timeoutAt = startTimeMillis + timeoutMillis 164 | waitUntilNewLog(timeoutAt - SystemClock.uptimeMillis()) 165 | waitUntilLogIdles(idleWindowMillis, timeoutAt - SystemClock.uptimeMillis()) 166 | } 167 | 168 | /** 169 | * Waits until new trace output has been written to the log file. 170 | * 171 | * @param timeoutMillis How long to keep waiting for, before throwing an error 172 | */ 173 | private fun waitUntilNewLog(timeoutMillis: Long = 10000) { 174 | val startTimeMillis = SystemClock.uptimeMillis() 175 | val timeoutAt = startTimeMillis + timeoutMillis 176 | while (true) { 177 | val size = traceOutput.length() 178 | if (size > lastTraceSize) { 179 | lastTraceSize = size 180 | return 181 | } 182 | 183 | if (SystemClock.uptimeMillis() >= timeoutAt) { 184 | throw TimeoutException("No trace output was received within $timeoutMillis ms") 185 | } 186 | Thread.sleep(100) 187 | } 188 | } 189 | 190 | /** 191 | * Waits until the log file stops receiving any trace output. 192 | * 193 | * @param idleWindowMillis How long the log file must stay the same size for, before it's considered idle 194 | * @param timeoutMillis How long to keep waiting for, before throwing an error 195 | */ 196 | private fun waitUntilLogIdles(idleWindowMillis: Long = 1000, timeoutMillis: Long = 10000) { 197 | val startTimeMillis = SystemClock.uptimeMillis() 198 | val timeoutAt = startTimeMillis + timeoutMillis 199 | 200 | lastTraceSize = traceOutput.length() 201 | Thread.sleep(idleWindowMillis) 202 | 203 | while (true) { 204 | val size = traceOutput.length() 205 | if (size == lastTraceSize) { 206 | return 207 | } 208 | lastTraceSize = size 209 | 210 | if (SystemClock.uptimeMillis() >= timeoutAt) { 211 | throw TimeoutException("No trace output was received within $timeoutMillis ms") 212 | } 213 | Thread.sleep(idleWindowMillis) 214 | } 215 | } 216 | } 217 | 218 | private fun UiDevice.click(point: Point) { 219 | this.click(point.x, point.y) 220 | } 221 | -------------------------------------------------------------------------------- /app/src/androidTest/java/rs/ruffle/SmokeTest.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.R 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import androidx.test.core.app.ApplicationProvider 9 | import androidx.test.espresso.matcher.ViewMatchers.assertThat 10 | import androidx.test.ext.junit.runners.AndroidJUnit4 11 | import androidx.test.platform.app.InstrumentationRegistry 12 | import androidx.test.uiautomator.By 13 | import androidx.test.uiautomator.UiDevice 14 | import androidx.test.uiautomator.Until 15 | import java.io.File 16 | import org.hamcrest.CoreMatchers.equalTo 17 | import org.hamcrest.CoreMatchers.notNullValue 18 | import org.junit.Before 19 | import org.junit.Test 20 | import org.junit.runner.RunWith 21 | 22 | private const val BASIC_SAMPLE_PACKAGE = "rs.ruffle" 23 | private const val LAUNCH_TIMEOUT = 5000L 24 | 25 | @RunWith(AndroidJUnit4::class) 26 | class SmokeTest { 27 | private lateinit var device: UiDevice 28 | private lateinit var traceOutput: File 29 | private lateinit var swfFile: File 30 | 31 | @Before 32 | fun startMainActivityFromHomeScreen() { 33 | // Initialize UiDevice instance 34 | device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 35 | 36 | // Start from the home screen 37 | device.pressHome() 38 | 39 | // Wait for launcher 40 | val launcherPackage: String = device.launcherPackageName 41 | assertThat(launcherPackage, notNullValue()) 42 | device.wait( 43 | Until.hasObject(By.pkg(launcherPackage).depth(0)), 44 | LAUNCH_TIMEOUT 45 | ) 46 | 47 | // Launch the app 48 | val context = ApplicationProvider.getApplicationContext() 49 | traceOutput = File.createTempFile("trace", ".txt", context.cacheDir) 50 | swfFile = File.createTempFile("movie", ".swf", context.cacheDir) 51 | val resources = InstrumentationRegistry.getInstrumentation().context.resources 52 | val inStream = resources.openRawResource( 53 | rs.ruffle.test.R.raw.helloflash 54 | ) 55 | val bytes = inStream.readBytes() 56 | swfFile.writeBytes(bytes) 57 | val intent = context.packageManager.getLaunchIntentForPackage( 58 | BASIC_SAMPLE_PACKAGE 59 | )?.apply { 60 | component = ComponentName("rs.ruffle", "rs.ruffle.PlayerActivity") 61 | data = Uri.fromFile(swfFile) 62 | putExtra("traceOutput", traceOutput.absolutePath) 63 | // Clear out any previous instances 64 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 65 | } 66 | context.startActivity(intent) 67 | 68 | // Wait for the app to appear 69 | device.wait( 70 | Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), 71 | LAUNCH_TIMEOUT 72 | ) 73 | } 74 | 75 | @Test 76 | fun emulatorRunsASwf() { 77 | device.waitForWindowUpdate(null, 1000) 78 | assertThat(device, notNullValue()) 79 | 80 | val trace = traceOutput.readLines() 81 | assertThat(trace, equalTo(listOf("Hello from Flash!"))) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/helloflash.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruffle-rs/ruffle-android/6959aa02fe363a045ef0dcd5d63b35d5c8136a8a/app/src/androidTest/res/raw/helloflash.swf -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/input_test.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruffle-rs/ruffle-android/6959aa02fe363a045ef0dcd5d63b35d5c8136a8a/app/src/androidTest/res/raw/input_test.swf -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/input_tests/Test.as: -------------------------------------------------------------------------------- 1 | package { 2 | import flash.display.MovieClip; 3 | import flash.events.MouseEvent; 4 | import flash.display.DisplayObject; 5 | import flash.events.KeyboardEvent; 6 | 7 | public class Test extends MovieClip { 8 | public function Test() { 9 | trace("Test started!"); 10 | 11 | addKeyListeners("stage", stage); 12 | addMouseListeners("red", red); 13 | addMouseListeners("blue", blue); 14 | } 15 | 16 | function addMouseListeners(name: String, clip: DisplayObject) { 17 | var listener = function(event: MouseEvent) { 18 | trace(name + " received " + event.type); 19 | }; 20 | clip.addEventListener(MouseEvent.MOUSE_DOWN, listener); 21 | clip.addEventListener(MouseEvent.MOUSE_UP, listener); 22 | //clip.addEventListener(MouseEvent.MOUSE_OVER, listener); 23 | //clip.addEventListener(MouseEvent.MOUSE_OUT, listener); 24 | clip.addEventListener(MouseEvent.CLICK, listener); 25 | } 26 | 27 | function addKeyListeners(name: String, clip: DisplayObject) { 28 | var listener = function(event: KeyboardEvent) { 29 | trace(event.type + ": keyCode = " + event.keyCode + ", charCode = " + event.charCode); 30 | }; 31 | stage.addEventListener(KeyboardEvent.KEY_DOWN, listener); 32 | stage.addEventListener(KeyboardEvent.KEY_UP, listener); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/input_tests/test.fla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruffle-rs/ruffle-android/6959aa02fe363a045ef0dcd5d63b35d5c8136a8a/app/src/androidTest/res/raw/input_tests/test.fla -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/input_tests/test.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruffle-rs/ruffle-android/6959aa02fe363a045ef0dcd5d63b35d5c8136a8a/app/src/androidTest/res/raw/input_tests/test.swf -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/rs/ruffle/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import rs.ruffle.ui.theme.RuffleTheme 10 | 11 | class MainActivity : ComponentActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | enableEdgeToEdge() 14 | super.onCreate(savedInstanceState) 15 | 16 | setContent { 17 | RuffleTheme { 18 | RuffleNavHost(openSwf = { openSwf(it) }) 19 | } 20 | } 21 | } 22 | 23 | private fun openSwf(uri: Uri) { 24 | val intent = Intent( 25 | this@MainActivity, 26 | PlayerActivity::class.java 27 | ).apply { 28 | data = uri 29 | } 30 | startActivity(intent) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/rs/ruffle/Navigation.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.net.Uri 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NavHostController 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.compose.rememberNavController 9 | 10 | object Destinations { 11 | const val SELECT_SWF_ROUTE = "select" 12 | } 13 | 14 | @Composable 15 | fun RuffleNavHost( 16 | navController: NavHostController = rememberNavController(), 17 | openSwf: (uri: Uri) -> Unit 18 | ) { 19 | NavHost( 20 | navController = navController, 21 | startDestination = Destinations.SELECT_SWF_ROUTE 22 | ) { 23 | composable(Destinations.SELECT_SWF_ROUTE) { 24 | SelectSwfRoute( 25 | openSwf = openSwf 26 | ) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/rs/ruffle/PanicActivity.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import rs.ruffle.ui.theme.RuffleTheme 8 | 9 | class PanicActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | enableEdgeToEdge() 12 | super.onCreate(savedInstanceState) 13 | 14 | setContent { 15 | RuffleTheme { 16 | PanicScreen(message = intent.getStringExtra("message") ?: "Unknown") 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/rs/ruffle/PanicScreen.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.horizontalScroll 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.wrapContentSize 11 | import androidx.compose.foundation.rememberScrollState 12 | import androidx.compose.foundation.text.selection.SelectionContainer 13 | import androidx.compose.foundation.verticalScroll 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import rs.ruffle.ui.theme.RuffleTheme 23 | 24 | @Composable 25 | fun PanicScreen(message: String) { 26 | Scaffold { innerPadding -> 27 | Column( 28 | modifier = Modifier 29 | .padding(innerPadding) 30 | .fillMaxSize(), 31 | verticalArrangement = Arrangement.Center 32 | ) { 33 | Text( 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .wrapContentSize(align = Alignment.Center), 37 | style = MaterialTheme.typography.headlineLarge, 38 | text = "Ruffle Panicked :(" 39 | ) 40 | SelectionContainer { 41 | Text( 42 | modifier = Modifier 43 | .wrapContentSize(align = Alignment.Center) 44 | .padding(horizontal = 8.dp, vertical = 20.dp) 45 | .verticalScroll(rememberScrollState()) 46 | .horizontalScroll(rememberScrollState()), 47 | text = message, 48 | softWrap = false 49 | ) 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Preview(name = "Panic - Light", uiMode = Configuration.UI_MODE_NIGHT_NO) 56 | @Preview(name = "Panic - Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) 57 | @Composable 58 | fun PanicScreenPreview() { 59 | RuffleTheme { 60 | PanicScreen( 61 | message = """Error: panicked at core/src/display_object/movie_clip.rs:477:9: 62 | assertion `left == right` failed: Called replace_movie on a clip with LoaderInfo set 63 | left: Some(LoaderInfoObject(LoaderInfoObject { ptr: 0x31b30a8 })) 64 | right: None 65 | at n.wbg.__wbg_new_796382978dfd4fb0 (https://unpkg.com/@ruffle-rs/ruffle/core.ruffle.90db0a0ab193ed0c601b.js:1:83857) 66 | at ruffle_web.wasm.js_sys::Error::new::hfb561c222a4e70eb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[12733]:0x98671a) 67 | at ruffle_web.wasm.core::ops::function::FnOnce::call_once{{vtable.shim}}::h8a2a563fa204b611 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9789]:0x9164aa) 68 | at ruffle_web.wasm.std::panicking::rust_panic_with_hook::h33fe77d38d305ca3 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[6355]:0x8070ed) 69 | at ruffle_web.wasm.core::panicking::panic_fmt::hde8b7aa66e2831e1 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9511]:0x9071fd) 70 | at ruffle_web.wasm.core::panicking::assert_failed_inner::hc95b7725cb4077cb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[4402]:0x73cb5e) 71 | at ruffle_web.wasm.ruffle_core::display_object::movie_clip::MovieClip::replace_with_movie::haf940b0718ed269c (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[2052]:0x50a035) 72 | at ruffle_web.wasm.ruffle_core::loader::Loader::movie_loader::{{closure}}::h566c935379317178 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1053]:0x2bc268) 73 | at ruffle_web.wasm.::spawn_future::{{closure}}::h13f3540dbe40e875 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1520]:0x419980) 74 | at ruffle_web.wasm.wasm_bindgen_futures::queue::Queue::new::{{closure}}::hf37247571cf9bbf7 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[3648]:0x6ba342)""" 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/rs/ruffle/PlayerActivity.kt: -------------------------------------------------------------------------------- 1 | package rs.ruffle 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.content.res.Configuration 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Build.VERSION_CODES 9 | import android.os.Bundle 10 | import android.util.Log 11 | import android.view.Menu 12 | import android.view.MenuItem 13 | import android.view.MotionEvent 14 | import android.view.View 15 | import android.view.ViewGroup 16 | import android.view.Window 17 | import android.view.WindowManager 18 | import android.widget.Button 19 | import android.widget.PopupMenu 20 | import androidx.constraintlayout.widget.ConstraintLayout 21 | import androidx.core.view.ViewCompat 22 | import androidx.core.view.WindowCompat 23 | import androidx.core.view.WindowInsetsCompat 24 | import androidx.core.view.WindowInsetsControllerCompat 25 | import com.google.androidgamesdk.GameActivity 26 | import java.io.DataInputStream 27 | import java.io.File 28 | import java.io.IOException 29 | 30 | class PlayerActivity : GameActivity() { 31 | @Suppress("unused") 32 | // Used by Rust 33 | private val swfBytes: ByteArray? 34 | get() { 35 | val uri = intent.data 36 | if (uri?.scheme == "content") { 37 | try { 38 | contentResolver.openInputStream(uri).use { inputStream -> 39 | if (inputStream == null) { 40 | return null 41 | } 42 | val bytes = ByteArray(inputStream.available()) 43 | val dataInputStream = DataInputStream(inputStream) 44 | dataInputStream.readFully(bytes) 45 | return bytes 46 | } 47 | } catch (ignored: IOException) { 48 | } 49 | } 50 | return null 51 | } 52 | 53 | @Suppress("unused") 54 | // Used by Rust 55 | private val swfUri: String? 56 | get() { 57 | return intent.dataString 58 | } 59 | 60 | @Suppress("unused") 61 | // Used by Rust 62 | private val traceOutput: String? 63 | get() { 64 | return intent.getStringExtra("traceOutput") 65 | } 66 | 67 | @Suppress("unused") 68 | // Used by Rust 69 | private fun navigateToUrl(url: String?) { 70 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 71 | } 72 | 73 | private var loc = IntArray(2) 74 | 75 | @Suppress("unused") 76 | // Handle of an EventLoopProxy over in rust-land 77 | private val eventLoopHandle: Long = 0 78 | 79 | @Suppress("unused") 80 | // Used by Rust 81 | private val locInWindow: IntArray 82 | get() { 83 | mSurfaceView.getLocationInWindow(loc) 84 | return loc 85 | } 86 | 87 | @Suppress("unused") 88 | // Used by Rust 89 | private val surfaceWidth: Int 90 | get() = mSurfaceView.width 91 | 92 | @Suppress("unused") 93 | // Used by Rust 94 | private val surfaceHeight: Int 95 | get() = mSurfaceView.height 96 | 97 | private external fun keydown(keyTag: String) 98 | private external fun keyup(keyTag: String) 99 | private external fun requestContextMenu() 100 | private external fun runContextMenuCallback(index: Int) 101 | private external fun clearContextMenu() 102 | 103 | @Suppress("unused") 104 | // Used by Rust 105 | private fun showContextMenu(items: Array) { 106 | runOnUiThread { 107 | val popup = PopupMenu(this, findViewById(R.id.button_cm)) 108 | val menu = popup.menu 109 | if (Build.VERSION.SDK_INT >= VERSION_CODES.P) { 110 | menu.setGroupDividerEnabled(true) 111 | } 112 | var group = 1 113 | for (i in items.indices) { 114 | val elements = items[i].split(" ".toRegex(), limit = 4).toTypedArray() 115 | val enabled = elements[0].toBoolean() 116 | val separatorBefore = elements[1].toBoolean() 117 | val checked = elements[2].toBoolean() 118 | val caption = elements[3] 119 | if (separatorBefore) group += 1 120 | val item = menu.add(group, i, Menu.NONE, caption) 121 | item.setEnabled(enabled) 122 | if (checked) { 123 | item.setCheckable(true) 124 | item.setChecked(true) 125 | } 126 | } 127 | val exitItemId: Int = items.size 128 | menu.add(group, exitItemId, Menu.NONE, "Exit") 129 | popup.setOnMenuItemClickListener { item: MenuItem -> 130 | if (item.itemId == exitItemId) { 131 | finish() 132 | } else { 133 | runContextMenuCallback(item.itemId) 134 | } 135 | true 136 | } 137 | popup.setOnDismissListener { clearContextMenu() } 138 | popup.show() 139 | } 140 | } 141 | 142 | @Suppress("unused") 143 | // Used by Rust 144 | private fun getAndroidDataStorageDir(): String { 145 | // TODO It can also be placed in an external storage path in the future to share archived content 146 | val storageDirPath = "${filesDir.absolutePath}/ruffle/shared_objects" 147 | val storageDir = File(storageDirPath) 148 | if (!storageDir.exists()) { 149 | storageDir.mkdirs() 150 | } 151 | return storageDirPath 152 | } 153 | 154 | override fun onCreateSurfaceView() { 155 | val inflater = layoutInflater 156 | 157 | @SuppressLint("InflateParams") 158 | val layout = inflater.inflate(R.layout.keyboard, null) as ConstraintLayout 159 | 160 | contentViewId = ViewCompat.generateViewId() 161 | layout.id = contentViewId 162 | setContentView(layout) 163 | mSurfaceView = InputEnabledSurfaceView(this) 164 | 165 | mSurfaceView.contentDescription = "Ruffle Player" 166 | 167 | val placeholder = findViewById(R.id.placeholder) 168 | val pars = placeholder.layoutParams as ConstraintLayout.LayoutParams 169 | val parent = placeholder.parent as ViewGroup 170 | val index = parent.indexOfChild(placeholder) 171 | parent.removeView(placeholder) 172 | parent.addView(mSurfaceView, index) 173 | mSurfaceView.setLayoutParams(pars) 174 | val keys = gatherAllDescendantsOfType