diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9284f8e7..c1d890b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: os: [macos-latest, windows-latest, ubuntu-latest] # Latest stable and MSRV. We only run checks with all features enabled # for the MSRV build to keep CI fast, since other configurations should also work. - rust_version: [stable, "1.71.0"] + rust_version: [stable, "1.77.0"] steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -34,6 +34,10 @@ jobs: components: clippy - uses: actions/checkout@v4 + - name: Add Android target + if: ${{ matrix.os == 'ubuntu-latest' }} + run: rustup target add x86_64-linux-android + - name: Run `cargo clippy` with no features if: ${{ matrix.rust_version == 'stable' }} run: cargo clippy --verbose --no-default-features -- -D warnings -D clippy::dbg_macro @@ -48,6 +52,14 @@ jobs: - name: Run `cargo clippy` with all features run: cargo clippy --verbose --all-features -- -D warnings -D clippy::dbg_macro + + - name: Run `cargo clippy` with no features on Android + if: ${{ matrix.os == 'ubuntu-latest' }} + run: cargo clippy --target x86_64-linux-android --verbose --no-default-features -- -D warnings -D clippy::dbg_macro + + - name: Run `cargo clippy` with `image-data` feature on Android + if: ${{ matrix.os == 'ubuntu-latest' }} + run: cargo clippy --target x86_64-linux-android --verbose --no-default-features --features image-data -- -D warnings -D clippy::dbg_macro - name: Run `cargo clippy` with dependency version checks if: ${{ matrix.rust_version == 'stable' }} @@ -89,7 +101,7 @@ jobs: steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: nightly-2023-10-08 + toolchain: nightly-2024-01-08 components: miri - name: Checkout @@ -104,3 +116,56 @@ jobs: - uses: actions/checkout@v4 - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@v2 + + android_emulator: + needs: clippy + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Install `cargo-apk` + run: cargo install cargo-apk + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-29 + + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + arch: x86_64 + target: default + profile: Nexus 6 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Run android example + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + force-avd-creation: false + arch: x86_64 + target: default + profile: Nexus 6 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: ./tools/android_test.sh diff --git a/Cargo.lock b/Cargo.lock index 23127c45..9c8cef2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,14 +17,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.8.0", + "cc", + "cesu8", + "jni 0.21.1", + "jni-sys 0.3.0", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "arboard" version = "3.6.1" dependencies = [ + "android-activity", "clipboard-win", "env_logger", "image", + "jni 0.22.0", "log", + "ndk", + "ndk-context", + "ndk-sys", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -32,7 +64,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys", + "windows-sys 0.52.0", "wl-clipboard-rs", "x11rb", ] @@ -67,11 +99,26 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" @@ -88,6 +135,16 @@ dependencies = [ "error-code", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -116,6 +173,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.13" @@ -123,7 +186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -164,12 +227,30 @@ dependencies = [ "windows-link", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -202,7 +283,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", ] [[package]] @@ -213,7 +304,90 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.0", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051ac7b04986c689b0c2e136bd26f992509d4d3e10aff8557ca002d5de7eae39" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "paste", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf8534090fdcb2891c0b24a46db1a11154747c83f59efd732d8fb75a7b2e48c" +dependencies = [ + "cesu8", + "proc-macro-crate", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", ] [[package]] @@ -252,9 +426,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" @@ -271,6 +445,36 @@ dependencies = [ "adler", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.8.0", + "jni-sys 0.3.0", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.0", +] + [[package]] name = "nom" version = "7.1.1" @@ -290,6 +494,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "objc2" version = "0.6.0" @@ -363,9 +589,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" @@ -374,7 +600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -397,9 +623,15 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -413,7 +645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 1.9.1", ] [[package]] @@ -434,6 +666,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -461,6 +702,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "redox_syscall" version = "0.5.8" @@ -487,6 +740,15 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.3" @@ -497,7 +759,22 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", ] [[package]] @@ -506,6 +783,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -532,13 +835,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -563,6 +886,36 @@ dependencies = [ "weezl", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "tree_magic_mini" version = "3.1.6" @@ -582,6 +935,25 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -664,7 +1036,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -673,13 +1045,37 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -688,28 +1084,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -722,30 +1136,69 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "wl-clipboard-rs" version = "0.9.3" @@ -756,7 +1209,7 @@ dependencies = [ "log", "os_pipe", "rustix", - "thiserror", + "thiserror 2.0.18", "tree_magic_mini", "wayland-backend", "wayland-client", diff --git a/Cargo.toml b/Cargo.toml index 07abd4f0..651f71ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["clipboard", "image"] edition = "2021" -rust-version = "1.71.0" +rust-version = "1.77.0" [features] default = ["image-data"] @@ -87,6 +87,10 @@ image = { version = "0.25", optional = true, default-features = false, features parking_lot = "0.12" percent-encoding = "2.3.1" +[target.'cfg(target_os = "android")'.dependencies] +ndk-context = "0.1" +jni = "0.22" + [[example]] name = "get_image" required-features = ["image-data"] @@ -94,3 +98,15 @@ required-features = ["image-data"] [[example]] name = "set_image" required-features = ["image-data"] + +[[example]] +name = "android" +crate-type = ["cdylib"] + +[target.'cfg(target_os = "android")'.dev-dependencies] +android-activity = { version = "0.6.0", features = [ "native-activity" ] } +ndk = "0.9" +ndk-sys = "0.6" + +[package.metadata.android.sdk] +target_sdk_version = 29 \ No newline at end of file diff --git a/examples/android.rs b/examples/android.rs new file mode 100644 index 00000000..1b38cc14 --- /dev/null +++ b/examples/android.rs @@ -0,0 +1,99 @@ +#[no_mangle] +#[cfg(target_os = "android")] +fn android_main(app: android_activity::AndroidApp) { + use android_activity::{MainEvent, PollEvent}; + use arboard::Clipboard; + + println!("app started"); + + let mut quit = false; + let mut redraw_pending = true; + let mut native_window: Option = None; + + let text = "hello world"; + + let mut ctx = Clipboard::new().unwrap(); + + assert!( + ctx.set_text(text).is_ok(), + "We can write to the clipboard even if we don't have focus" + ); + + assert!(ctx.get_text().is_err(), "We can't read the clipboard if we don't have focus"); + + while !quit { + app.poll_events(Some(std::time::Duration::from_secs(1)) /* timeout */, |event| { + match event { + PollEvent::Wake => {} + PollEvent::Timeout => { + redraw_pending = true; + } + PollEvent::Main(main_event) => { + match main_event { + MainEvent::InitWindow { .. } => { + native_window = app.native_window(); + redraw_pending = true; + } + MainEvent::TerminateWindow { .. } => { + native_window = None; + } + MainEvent::WindowResized { .. } => { + redraw_pending = true; + } + MainEvent::RedrawNeeded { .. } => { + redraw_pending = true; + } + MainEvent::InputAvailable { .. } => { + redraw_pending = true; + } + MainEvent::GainedFocus => { + assert_eq!( + ctx.get_text().unwrap(), + text, + "Since we have focus we can access the clipboard" + ); + + ctx.clear().unwrap(); + assert!(ctx.get_text().is_err()); + + quit = true; + } + _ => { /* ... */ } + } + } + _ => {} + } + + if redraw_pending { + if let Some(native_window) = &native_window { + redraw_pending = false; + + dummy_render(native_window); + } + } + }); + } +} + +/// Post a NOP frame to the window +/// +/// Since this is a bare minimum test app we don't depend +/// on any GPU graphics APIs but we do need to at least +/// convince Android that we're drawing something and are +/// responsive, otherwise it will stop delivering input +/// events to us. +#[cfg(target_os = "android")] +fn dummy_render(native_window: &ndk::native_window::NativeWindow) { + unsafe { + let mut buf: ndk_sys::ANativeWindow_Buffer = std::mem::zeroed(); + let mut rect: ndk_sys::ARect = std::mem::zeroed(); + ndk_sys::ANativeWindow_lock( + native_window.ptr().as_ptr() as _, + &mut buf as _, + &mut rect as _, + ); + // Note: we don't try and touch the buffer since that + // also requires us to handle various buffer formats + ndk_sys::ANativeWindow_unlockAndPost(native_window.ptr().as_ptr() as _); + } +} diff --git a/src/common.rs b/src/common.rs index b5e877ba..2c41c607 100644 --- a/src/common.rs +++ b/src/common.rs @@ -90,6 +90,7 @@ impl std::fmt::Debug for Error { } impl Error { + #[cfg(not(target_os = "android"))] pub(crate) fn unknown>(message: M) -> Self { Error::Unknown { description: message.into() } } @@ -149,12 +150,12 @@ impl ImageData<'_> { } } -#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +#[cfg(any(windows, all(unix, not(any(target_os = "macos", target_os = "android")))))] pub(crate) struct ScopeGuard { callback: Option, } -#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +#[cfg(any(windows, all(unix, not(any(target_os = "macos", target_os = "android")))))] impl ScopeGuard { #[cfg_attr(all(windows, not(feature = "image-data")), allow(dead_code))] pub(crate) fn new(callback: F) -> Self { @@ -162,7 +163,7 @@ impl ScopeGuard { } } -#[cfg(any(windows, all(unix, not(target_os = "macos"))))] +#[cfg(any(windows, all(unix, not(any(target_os = "macos", target_os = "android")))))] impl Drop for ScopeGuard { fn drop(&mut self) { if let Some(callback) = self.callback.take() { @@ -172,6 +173,7 @@ impl Drop for ScopeGuard { } /// Common trait for sealing platform extension traits. +#[cfg(not(target_os = "android"))] pub(crate) mod private { pub trait Sealed {} diff --git a/src/platform/android.rs b/src/platform/android.rs new file mode 100644 index 00000000..eae50c9c --- /dev/null +++ b/src/platform/android.rs @@ -0,0 +1,197 @@ +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use jni::{ + jni_sig, jni_str, + objects::{JObject, JString}, + Env, JavaVM, +}; + +#[cfg(feature = "image-data")] +use crate::common::ImageData; +use crate::Error; + +impl From for Error { + fn from(error: jni::errors::Error) -> Self { + Error::Unknown { description: error.to_string() } + } +} + +fn with_clipboard_access(callback: F) -> Result +where + F: FnOnce(&mut Env, JObject) -> Result, +{ + let ctx = ndk_context::android_context(); + + let jvm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }; + + jvm.attach_current_thread(|env| { + let context = unsafe { JObject::from_raw(env, ctx.context().cast()) }; + let clipboard = env.new_string("clipboard")?; + + let clipboard_manager = env + .call_method( + context, + jni_str!("getSystemService"), + jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"), + &[(&clipboard).into()], + )? + .l()?; + + callback(env, clipboard_manager) + }) +} + +pub(crate) struct Clipboard(()); + +impl Clipboard { + pub(crate) fn new() -> Result { + with_clipboard_access(|env, _| { + let version_class = env.load_class(jni_str!("android/os/Build$VERSION"))?; + let build_sdk = + env.get_static_field(version_class, jni_str!("SDK_INT"), jni_sig!("I"))?.i()?; + + // clearPrimaryClip was introduced in this version + if build_sdk >= 28 { + Ok(Self(())) + } else { + Err(Error::ClipboardNotSupported) + } + }) + } +} + +pub(crate) struct Get<'clipboard> { + _clipboard: &'clipboard Clipboard, +} + +impl<'clipboard> Get<'clipboard> { + pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self { + Self { _clipboard } + } + + pub(crate) fn text(self) -> Result { + with_clipboard_access(|env, clipboard_manager| { + if !env + .call_method(&clipboard_manager, jni_str!("hasPrimaryClip"), jni_sig!("()Z"), &[])? + .z()? + { + return Err(Error::ContentNotAvailable); + } + + let clip = env + .call_method( + clipboard_manager, + jni_str!("getPrimaryClip"), + jni_sig!("()Landroid/content/ClipData;"), + &[], + )? + .l()?; + + if env.call_method(&clip, jni_str!("getItemCount"), jni_sig!("()I"), &[])?.i()? == 0 { + return Err(Error::ContentNotAvailable); + } + + let item = env + .call_method( + &clip, + jni_str!("getItemAt"), + jni_sig!("(I)Landroid/content/ClipData$Item;"), + &[0.into()], + )? + .l()?; + + let char_sequence = env + .call_method( + item, + jni_str!("getText"), + jni_sig!("()Ljava/lang/CharSequence;"), + &[], + )? + .l()?; + let text = env.cast_local::(char_sequence)?.to_string(); + + Ok(text) + }) + } + + pub(crate) fn html(self) -> Result { + Err(Error::ClipboardNotSupported) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self) -> Result, Error> { + Err(Error::ClipboardNotSupported) + } + + pub(crate) fn file_list(self) -> Result, Error> { + Err(Error::ClipboardNotSupported) + } +} + +pub(crate) struct Set<'clipboard> { + _clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Set<'clipboard> { + pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self { + Self { _clipboard } + } + + pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> { + with_clipboard_access(|env, clipboard_manager| { + let label = env.new_string("label")?; + let text = env.new_string(text)?; + + let clip_data = env.call_static_method( + jni_str!("android/content/ClipData"), + jni_str!("newPlainText"), + jni_sig!( + "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;" + ), + &[(&label).into(), (&text).into()], + )?; + + env.call_method( + clipboard_manager, + jni_str!("setPrimaryClip"), + jni_sig!("(Landroid/content/ClipData;)V"), + &[(&clip_data).into()], + )?; + + Ok(()) + }) + } + + pub(crate) fn html(self, _: Cow<'_, str>, _: Option>) -> Result<(), Error> { + Err(Error::ClipboardNotSupported) + } + + #[cfg(feature = "image-data")] + pub(crate) fn image(self, _: ImageData) -> Result<(), Error> { + Err(Error::ClipboardNotSupported) + } + + pub(crate) fn file_list(self, _: &[impl AsRef]) -> Result<(), Error> { + Err(Error::ClipboardNotSupported) + } +} + +pub(crate) struct Clear<'clipboard> { + _clipboard: &'clipboard mut Clipboard, +} + +impl<'clipboard> Clear<'clipboard> { + pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self { + Self { _clipboard } + } + + pub(crate) fn clear(self) -> Result<(), Error> { + with_clipboard_access(|env, clipboard_manager| { + env.call_method(clipboard_manager, jni_str!("clearPrimaryClip"), jni_sig!("()V"), &[])?; + Ok(()) + }) + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 268eb47e..5180a704 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -15,3 +15,8 @@ pub use windows::*; mod osx; #[cfg(target_os = "macos")] pub use osx::*; + +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "android")] +pub(crate) use android::*; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index cb115982..93e9fd5f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -747,10 +747,9 @@ impl<'clipboard> Set<'clipboard> { let paths: Vec<_> = file_list .iter() .filter_map(|path| { - to_final_path_wide(path.as_ref()).map(|wide| { + to_final_path_wide(path.as_ref()).inspect(|wide| { // Windows uses wchar_t which is 16 bit data_len += wide.len() * std::mem::size_of::(); - wide }) }) .collect(); diff --git a/tools/android_test.sh b/tools/android_test.sh new file mode 100755 index 00000000..dd978b38 --- /dev/null +++ b/tools/android_test.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# This script is used in CI to run a sample Android app +# and verify arboard implementation for the platform. + +# Make sure the package is removed since it may end up in the AVD cache. This causes +# INSTALL_FAILED_UPDATE_INCOMPATIBLE errors when the debug keystore is regenerated, +# as it is not stored/cached on the CI +adb uninstall rust.example.android || true + +cargo apk run --target x86_64-linux-android --example android --no-logcat + +sleep 30 + +adb logcat *:I -d > ~/logcat.log + +if grep 'app started' ~/logcat.log; +then + echo "App running" +else + echo "::error::App not running" + exit 1 +fi + +ERROR_MSG=$(grep -e "thread '.*' panicked at" ~/logcat.log) +if [ -z "${ERROR_MSG}" ]; +then + exit 0 +else + echo "::error::${ERROR_MSG}" + exit 1 +fi