diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a81ed291..3a23e517 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,14 +39,14 @@ jobs: CARGO_BUILD_TARGET: ${{ matrix.cbt }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update - sudo apt-get install libdbus-1-dev - - uses: actions/setup-java@v2 - if: ${{ matrix.target == 'android' }} + sudo apt-get install libdbus-1-dev openjdk-17-jdk + - uses: actions/setup-java@v5 + if: ${{ matrix.target == 'android' || matrix.target == 'linux' }} with: distribution: 'zulu' java-version: '17' @@ -71,20 +71,29 @@ jobs: run: cargo check --all --bins --examples --no-default-features - name: Check with all features run: cargo check --all --bins --examples --all-features + - name: Run JNI host tests + if: ${{ matrix.target == 'linux' }} + run: ./scripts/run-jni-tests.sh - name: Run tests if: ${{ matrix.target != 'android' }} run: cargo test --all + - name: Build Android library and test app + if: ${{ matrix.target == 'android' }} + run: | + ./scripts/build-java.sh + cp src/droidplug/java/gradlew tests/android/ + cp -r src/droidplug/java/gradle tests/android/ + printf 'sdk.dir=%s\n' "$ANDROID_HOME" > tests/android/local.properties + cd tests/android + chmod +x gradlew + ./gradlew assembleDebug assembleAndroidTest - name: Run clippy - uses: actions-rs/clippy-check@v1 - with: - name: clippy ${{ matrix.os }} - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + run: cargo clippy --all-features -- -D warnings format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Format Rust code run: cargo fmt --all -- --check @@ -100,7 +109,7 @@ jobs: # env: # RUSTC_BOOTSTRAP: 1 # steps: -# - uses: actions/checkout@v2 +# - uses: actions/checkout@v4 # - name: Install dependencies # if: ${{ runner.os == 'Linux' }} # run: sudo apt-get install libdbus-1-dev diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5b455f..747af2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +# 0.13.0 (2026-08-29) + +## Features + +- Add `Central::retrieve_peripherals()` and `RetrievePeripheralsOptions` for + retrieving peripherals by identifier or advertised service without scanning. + This is supported on Linux, macOS/iOS, and Windows; Android returns + `Error::NotSupported`. +- Add `Central::adapter_address()` for retrieving the local adapter address on + Linux and Windows. Apple platforms and ordinary Android applications return + `Ok(None)` because their public APIs do not expose this address. +- Implement `Central::add_peripheral()` on Windows, allowing bonded or already + connected devices to be reached by address without waiting for an + advertisement. +- Add `appearance` to `PeripheralProperties`, populated from GAP Appearance + advertising data on Windows, Linux, and Android. CoreBluetooth does not expose + this advertising field, so it remains `None` on Apple platforms. +- Add support for receiving advertisements on the Bluetooth LE Coded PHY on + Windows where supported. + +## Bugfixes + +- Fix CoreBluetooth service discovery hanging when descriptor discovery fails. +- Fix CoreBluetooth `clear_peripherals()` so cleared devices can be rediscovered + and emit fresh discovery events. +- Report the negotiated CoreBluetooth MTU after service discovery instead of + always returning the default MTU. +- Preserve complete local names over shortened names across split or repeated + advertisements on Android, macOS/iOS, and Windows. +- Fix filtered Windows scans dropping scan-response packets that omit service + UUIDs, and prevent stale scan-response matches from leaking between scans. +- Propagate Android JNI callback and initialization failures consistently while + preserving Java exception details. +- Harden Windows characteristic subscription state so repeated subscriptions do + not install duplicate handlers and failed CCCD operations remain retryable. + (#326) +- Clarify that `Peripheral::properties()` is a backend-dependent snapshot and + may be unavailable, incomplete, or stale. (#339) +- Prevent CoreBluetooth descriptor discovery and GATT operation failures from + hanging pending futures or panicking on missing relationships. (#397, #422) +- Run compatible CoreBluetooth operations through FIFO queues, preserve + write-without-response ordering under backpressure, and complete pending + operations safely on disconnect or late callbacks. (#464) +- Remove stale CoreBluetooth peripheral event senders after dispatch failures so + peripherals can be rediscovered cleanly. (#469) +- Expand Android JNI host-test coverage for futures, streams, and environment + setup across worker threads. (#427) +- Update the event-driven discovery example to describe its async-task usage. + (#460) + +## Breaking Changes + +- **Android minimum SDK**: Android API 24 (Android 7.0) or newer is now required. +- **`PeripheralProperties` struct literals**: The new `appearance` field must be + initialized by callers that construct this public struct directly. + +## Dependencies + +- Update `jni` from 0.19 to 0.22 and migrate the Android backend to its current + API. + # 0.12.0 (2026-03-08) ## Features diff --git a/Cargo.lock b/Cargo.lock index b586906e..50d3533e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,26 +19,26 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block2" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] @@ -58,7 +58,7 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror 2.0.18", + "thiserror", "tokio", "uuid", ] @@ -96,7 +96,7 @@ dependencies = [ "serde_bytes", "serde_json", "static_assertions", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "toml", @@ -107,21 +107,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cesu8" -version = "1.1.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cfg-if" @@ -161,15 +155,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -181,15 +175,15 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -236,9 +230,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -251,9 +245,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -261,15 +255,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -278,38 +272,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -336,6 +330,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.14.5" @@ -353,9 +353,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -383,12 +383,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -401,7 +401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -415,37 +415,78 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "java-locator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c46c1fe465c59b1474e665e85e1256c3893dd00927b8d55f63b09044c1e64f" +dependencies = [ + "glob", +] [[package]] name = "jni" -version = "0.19.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", + "cfg-if", "combine", + "java-locator", + "jni-macros", "jni-sys", + "libloading", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +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 2.0.119", +] [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -463,9 +504,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -476,6 +517,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -487,48 +538,41 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys 0.61.2", + "windows-sys", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - [[package]] name = "objc2" -version = "0.5.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "objc-sys", "objc2-encode", ] [[package]] name = "objc2-core-bluetooth" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +checksum = "79b30b9eacc37434a61377866f68b27acef9fa5496f25cc9ca8b2549032e0394" dependencies = [ "bitflags", "objc2", @@ -543,21 +587,20 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags", "block2", - "libc", "objc2", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot_core" @@ -580,9 +623,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "pretty_env_logger" @@ -601,23 +644,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -683,11 +726,20 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -712,9 +764,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -728,7 +780,7 @@ checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror 2.0.18", + "thiserror", "xml", ] @@ -744,29 +796,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -777,13 +829,29 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -792,18 +860,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -814,9 +882,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -824,84 +892,75 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "syn" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ - "winapi-util", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "termcolor" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ - "thiserror-impl 1.0.69", + "winapi-util", ] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "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", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "tokio" -version = "1.50.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -911,9 +970,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -924,9 +983,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -939,27 +998,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "unicode-ident" @@ -975,9 +1034,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.22.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "js-sys", "serde_core", @@ -1020,9 +1079,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1033,9 +1092,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1043,22 +1102,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -1103,7 +1162,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -1159,7 +1218,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1170,7 +1229,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1207,24 +1266,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1234,39 +1275,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "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-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -1276,107 +1284,11 @@ dependencies = [ "windows-link", ] -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -1408,7 +1320,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.119", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1424,7 +1336,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1474,6 +1386,6 @@ checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index c117aa21..8f71d5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,35 +26,35 @@ serde = ["uuid/serde", "serde_cr", "serde_bytes"] jni-host-tests = ["jni/invocation", "once_cell"] [dependencies] -async-trait = "0.1.89" -log = "0.4.29" -bitflags = "2.11.0" -thiserror = "2.0.18" -uuid = "1.22.0" -serde_cr = { package = "serde", version = "1.0.228", features = ["derive"], default-features = false, optional = true } +async-trait = "0.1.92" +log = "0.4.34" +bitflags = "2.13.1" +thiserror = "2.0.20" +uuid = "1.26.0" +serde_cr = { package = "serde", version = "1.0.229", features = ["derive"], default-features = false, optional = true } serde_bytes = { version = "0.11.19", optional = true } -dashmap = "6.1.0" -futures = "0.3.32" +dashmap = "6.2.1" +futures = "0.3.34" static_assertions = "1.1.0" # rt feature needed for block_on in macOS internal thread -tokio = { version = "1.50.0", features = ["sync", "rt", "time"] } -tokio-stream = { version = "0.1.18", features = ["sync"] } +tokio = { version = "1.53.1", features = ["sync", "rt", "time"] } +tokio-stream = { version = "0.1.19", features = ["sync"] } [target.'cfg(target_os = "linux")'.dependencies] -dbus = "0.9.10" +dbus = "0.9.12" bluez-async = "0.8.2" -[target.'cfg(target_os = "android")'.dependencies] -jni = "0.19.0" -once_cell = "1.21.3" - [target.'cfg(not(target_os = "android"))'.dependencies] -jni = { version = "0.19.0", optional = true } -once_cell = { version = "1.21.3", optional = true } +jni = { version = "0.22", optional = true } +once_cell = { version = "1.21.4", optional = true } + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.22" +once_cell = "1.21.4" [target.'cfg(target_vendor = "apple")'.dependencies] -objc2 = "0.5.2" -objc2-foundation = { version = "0.2.2", default-features = false, features = [ +objc2 = "0.6.4" +objc2-foundation = { version = "0.3.2", default-features = false, features = [ "std", "block2", "NSArray", @@ -67,7 +67,7 @@ objc2-foundation = { version = "0.2.2", default-features = false, features = [ "NSUUID", "NSValue", ] } -objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = [ +objc2-core-bluetooth = { version = "0.3.2", default-features = false, features = [ "std", "CBAdvertisementData", "CBAttribute", @@ -83,14 +83,14 @@ objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = ] } [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.62", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } +windows = { version = "0.62", features = ["Devices_Bluetooth", "Devices_Enumeration", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } windows-future = "0.3.2" [dev-dependencies] rand = "0.10" pretty_env_logger = "0.5.0" -tokio = { version = "1.50.0", features = ["macros", "rt", "rt-multi-thread"] } -serde_json = "1.0.149" -toml = "1.0.6" +tokio = { version = "1.53.1", features = ["macros", "rt", "rt-multi-thread"] } +serde_json = "1.0.151" +toml = "1.1.4" anyhow = "1" lazy_static = "1.5.0" diff --git a/README.md b/README.md index 5abfac2a..d43b0c82 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ support. | └ Discover Manufacturer Data | X | X | X | X | | └ Discover Service Data | X | X | X | X | | └ Discover MAC address | X | | X | X | +| Retrieve local adapter address | X | | X | | | GATT Server Connect | X | X | X | X | | GATT Server Connect Event | X | X | X | X | | GATT Server Disconnect | X | X | X | X | @@ -74,6 +75,8 @@ support. #### Scan Filtering on Linux (BlueZ) +The `Central::adapter_address()` API retrieves a local Bluetooth adapter address only where the platform exposes one (currently Linux and Windows). CoreBluetooth and ordinary Android applications return `Ok(None)` because their public APIs provide opaque or privacy-restricted adapter identities. This address is distinct from a discovered peripheral address; `PeripheralId` remains the portable identity for adapters' peripherals. + The `ScanFilter` passed to `start_scan()` behaves differently on Linux than other platforms. btleplug forwards service UUID filters to BlueZ, but BlueZ [merges discovery filters across all D-Bus clients](https://github.com/bluez/bluez/blob/290f9973c9069f293367284e95fd338a221ab90d/doc/org.bluez.Adapter.rst?plain=1#L171-L173). @@ -111,6 +114,12 @@ Privacy_ → _Privacy_ → _Bluetooth_, clicking the '+' button, and selecting Due to requiring a hybrid Rust/Java build, btleplug for Android requires a somewhat complicated setup. +The Android library and its test application require Android API 24 (Android 7.0) or newer. This is +because the JNI support classes use Java functional interfaces introduced in API 24. Applications +that need to support API 23 must use a compatible btleplug release or provide a redesigned JNI +interface; core library desugaring is not enabled by btleplug and would also need to be configured +by the consuming application. + There is now a build script at `./scripts/build-java.sh` for building the java portion of the library on linux or macOS. This can also be used as a guide for manual building if need be. If your app uses Proguard/R8 with `minifyEnabled true`, you must add keep rules for btleplug's diff --git a/examples/discover_adapters_peripherals.rs b/examples/discover_adapters_peripherals.rs index 2bacfea2..a95d12d9 100644 --- a/examples/discover_adapters_peripherals.rs +++ b/examples/discover_adapters_peripherals.rs @@ -41,7 +41,7 @@ async fn main() -> anyhow::Result<()> { local_name, is_connected ); if !is_connected { - println!("Connecting to peripheral {:?}...", &local_name); + println!("Connecting to peripheral {:?}...", local_name); if let Err(err) = peripheral.connect().await { eprintln!("Error connecting to peripheral, skipping: {}", err); continue; @@ -50,10 +50,10 @@ async fn main() -> anyhow::Result<()> { let is_connected = peripheral.is_connected().await?; println!( "Now connected ({:?}) to peripheral {:?}...", - is_connected, &local_name + is_connected, local_name ); peripheral.discover_services().await?; - println!("Discover peripheral {:?} services...", &local_name); + println!("Discover peripheral {:?} services...", local_name); for service in peripheral.services() { println!( "Service UUID {}, primary: {}", @@ -64,7 +64,7 @@ async fn main() -> anyhow::Result<()> { } } if is_connected { - println!("Disconnecting from peripheral {:?}...", &local_name); + println!("Disconnecting from peripheral {:?}...", local_name); peripheral .disconnect() .await diff --git a/examples/event_driven_discovery.rs b/examples/event_driven_discovery.rs index fbc48b43..e16d6fd7 100644 --- a/examples/event_driven_discovery.rs +++ b/examples/event_driven_discovery.rs @@ -9,7 +9,7 @@ use futures::stream::StreamExt; async fn get_central(manager: &Manager) -> Adapter { let adapters = manager.adapters().await.unwrap(); - adapters.into_iter().nth(0).unwrap() + adapters.into_iter().next().unwrap() } #[tokio::main] @@ -33,9 +33,8 @@ async fn main() -> anyhow::Result<()> { // start scanning for devices central.start_scan(ScanFilter::default()).await?; - // Print based on whatever the event receiver outputs. Note that the event - // receiver blocks, so in a real program, this should be run in its own - // thread (not task, as this library does not yet use async channels). + // Process events asynchronously. In a real program, run this loop in its own + // Tokio task if event handling should proceed independently of other work. while let Some(event) = events.next().await { match event { CentralEvent::DeviceDiscovered(id) => { diff --git a/examples/lights.rs b/examples/lights.rs index 6523cb59..35316671 100644 --- a/examples/lights.rs +++ b/examples/lights.rs @@ -40,7 +40,7 @@ async fn main() -> anyhow::Result<()> { .await .expect("Unable to fetch adapter list.") .into_iter() - .nth(0) + .next() .expect("Unable to find adapters."); // start scanning for devices @@ -78,7 +78,7 @@ async fn main() -> anyhow::Result<()> { 0xAA, ]; light - .write(&cmd_char, &color_cmd, WriteType::WithoutResponse) + .write(cmd_char, &color_cmd, WriteType::WithoutResponse) .await?; time::sleep(Duration::from_millis(200)).await; } diff --git a/examples/subscribe_notify_characteristic.rs b/examples/subscribe_notify_characteristic.rs index cd661b53..0f763ffc 100644 --- a/examples/subscribe_notify_characteristic.rs +++ b/examples/subscribe_notify_characteristic.rs @@ -11,7 +11,7 @@ use uuid::Uuid; /// Only devices whose name contains this string will be tried. const PERIPHERAL_NAME_MATCH_FILTER: &str = "Neuro"; /// UUID of the characteristic for which we should subscribe to notifications. -const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dccA9e); +const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dcca9e); #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -45,11 +45,11 @@ async fn main() -> anyhow::Result<()> { .unwrap_or(String::from("(peripheral name unknown)")); println!( "Peripheral {:?} is connected: {:?}", - &local_name, is_connected + local_name, is_connected ); // Check if it's the peripheral we want. if local_name.contains(PERIPHERAL_NAME_MATCH_FILTER) { - println!("Found matching peripheral {:?}...", &local_name); + println!("Found matching peripheral {:?}...", local_name); if !is_connected { // Connect if we aren't already connected. if let Err(err) = peripheral.connect().await { @@ -60,7 +60,7 @@ async fn main() -> anyhow::Result<()> { let is_connected = peripheral.is_connected().await?; println!( "Now connected ({:?}) to peripheral {:?}.", - is_connected, &local_name + is_connected, local_name ); if is_connected { println!("Discover peripheral {:?} services...", local_name); diff --git a/scripts/run-jni-tests.sh b/scripts/run-jni-tests.sh index 6abb7147..b312228c 100755 --- a/scripts/run-jni-tests.sh +++ b/scripts/run-jni-tests.sh @@ -13,7 +13,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" JAVA_SRC_DIR="$PROJECT_ROOT/src/droidplug/java/src/main/java" -BUILD_DIR="$PROJECT_ROOT/target/debug/java" +CARGO_TARGET_ROOT="${CARGO_TARGET_DIR:-$PROJECT_ROOT/target}" +HOST_TARGET="$(rustc -vV | sed -n 's/^host: //p')" +BUILD_DIR="$CARGO_TARGET_ROOT/$HOST_TARGET/debug/java" JAR_DIR="$BUILD_DIR/libs" JAR_PATH="$JAR_DIR/btleplug-jni.jar" @@ -97,16 +99,16 @@ compile_java() { rm -rf "$classes_dir" mkdir -p "$classes_dir" - # Only compile the gedgygedgy sources — the nonpolynomial sources - # depend on Android APIs and cannot be compiled with plain javac. - local gedgy_dir="$JAVA_SRC_DIR/io/github/gedgygedgy" + # Only compile the plain-Java rust support classes — the nonpolynomial + # sources depend on Android APIs and cannot be compiled with plain javac. + local rust_dir="$JAVA_SRC_DIR/io/github/gedgygedgy/rust" local sources=() while IFS= read -r -d '' f; do sources+=("$f") - done < <(find "$gedgy_dir" -name '*.java' -print0) + done < <(find "$rust_dir" -name '*.java' -print0) if [ ${#sources[@]} -eq 0 ]; then - die "No .java files found under $gedgy_dir" + die "No .java files found under $rust_dir" fi info "Found ${#sources[@]} Java source files" @@ -132,7 +134,7 @@ run_tests() { info "Running jni_utils tests..." cd "$PROJECT_ROOT" - cargo test --features jni-host-tests -- --test-threads=1 + cargo test --target "$HOST_TARGET" --features jni-host-tests -- --test-threads=1 info "All tests passed!" } diff --git a/src/advertisement.rs b/src/advertisement.rs new file mode 100644 index 00000000..d720d0ed --- /dev/null +++ b/src/advertisement.rs @@ -0,0 +1,115 @@ +// btleplug Source Code File +// +// Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +/// GAP Appearance advertising data type. +pub(crate) const APPEARANCE_DATA_TYPE: u8 = 0x19; + +/// Parse the payload of a GAP Appearance advertising data section. +pub(crate) fn parse_appearance(data: &[u8]) -> Option { + let bytes: [u8; 2] = data.try_into().ok()?; + Some(u16::from_le_bytes(bytes)) +} + +/// Parse GAP Appearance from a length-prefixed Bluetooth LE advertising record. +#[cfg(any(target_os = "android", test))] +pub(crate) fn parse_appearance_from_advertisement(data: &[u8]) -> Option { + let mut offset = 0; + + while let Some(&length) = data.get(offset) { + offset += 1; + + if length == 0 { + break; + } + + let end = offset.checked_add(usize::from(length))?; + let section = data.get(offset..end)?; + let (&data_type, payload) = section.split_first()?; + + if data_type == APPEARANCE_DATA_TYPE + && let Some(appearance) = parse_appearance(payload) + { + return Some(appearance); + } + + offset = end; + } + + None +} + +#[cfg(test)] +mod tests { + use super::{APPEARANCE_DATA_TYPE, parse_appearance, parse_appearance_from_advertisement}; + + #[test] + fn parses_appearance_payload_as_little_endian() { + assert_eq!(parse_appearance(&[0x80, 0x04]), Some(0x0480)); + assert_eq!(parse_appearance(&[0x00, 0x00]), Some(0x0000)); + } + + #[test] + fn rejects_appearance_payloads_that_are_not_exactly_two_bytes() { + assert_eq!(parse_appearance(&[]), None); + assert_eq!(parse_appearance(&[0x80]), None); + assert_eq!(parse_appearance(&[0x80, 0x04, 0x00]), None); + } + + #[test] + fn finds_appearance_among_other_advertising_sections() { + let advertisement = [ + 2, + 0x01, + 0x06, + 3, + APPEARANCE_DATA_TYPE, + 0x80, + 0x04, + 2, + 0x0a, + 0xf8, + ]; + + assert_eq!( + parse_appearance_from_advertisement(&advertisement), + Some(0x0480) + ); + } + + #[test] + fn skips_invalid_appearance_section_and_accepts_a_later_valid_one() { + let advertisement = [ + 2, + APPEARANCE_DATA_TYPE, + 0x80, + 3, + APPEARANCE_DATA_TYPE, + 0x40, + 0x03, + ]; + + assert_eq!( + parse_appearance_from_advertisement(&advertisement), + Some(0x0340) + ); + } + + #[test] + fn handles_missing_and_malformed_advertising_sections() { + assert_eq!(parse_appearance_from_advertisement(&[]), None); + assert_eq!(parse_appearance_from_advertisement(&[0]), None); + assert_eq!(parse_appearance_from_advertisement(&[2, 0x01, 0x06]), None); + assert_eq!( + parse_appearance_from_advertisement(&[3, APPEARANCE_DATA_TYPE, 0x80,]), + None + ); + assert_eq!( + parse_appearance_from_advertisement(&[0, 3, APPEARANCE_DATA_TYPE, 0x80, 0x04,]), + None + ); + } +} diff --git a/src/api/bdaddr.rs b/src/api/bdaddr.rs index aef1ce3e..fcb5651c 100644 --- a/src/api/bdaddr.rs +++ b/src/api/bdaddr.rs @@ -476,6 +476,11 @@ mod tests { ); } + #[test] + fn zero_u64_to_addr_is_zero_sentinel() { + assert_eq!(BDAddr::try_from(0), Ok(BDAddr::default())); + } + #[test] fn addr_to_u64() { let addr_as_hex: u64 = ADDR.into(); diff --git a/src/api/mod.rs b/src/api/mod.rs index 5986714e..5ba3557f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -33,8 +33,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "serde")] use serde_cr as serde; use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeSet, HashMap, HashSet}, fmt::{self, Debug, Display, Formatter}, + hash::Hash, pin::Pin, time::Duration, }; @@ -189,6 +190,9 @@ pub struct PeripheralProperties { pub local_name: Option, /// The advertisement name. May be different than local_name. pub advertisement_name: Option, + /// The GAP appearance reported by this peripheral. + #[cfg_attr(feature = "serde", serde(default))] + pub appearance: Option, /// The transmission power level for the device pub tx_power_level: Option, /// The most recent Received Signal Strength Indicator for the device @@ -217,6 +221,82 @@ pub struct ScanFilter { pub services: Vec, } +/// Selects peripherals for [`Central::retrieve_peripherals`]. +/// +/// `None` leaves a selector unspecified; an explicitly empty selector matches nothing. Values +/// within a selector are OR'ed, while the identifier and service selectors are combined as a +/// union. Returned peripherals retain backend order and are deduplicated by identifier. +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct RetrievePeripheralsOptions { + /// Known peripheral identifiers to retrieve. + pub identifiers: Option>, + /// Service UUIDs used to retrieve connected peripherals. + pub services: Option>, +} + +/// Returns whether a candidate identifier is included in an identifier selector. +#[allow(dead_code)] // Used by platform-gated backend implementations. +pub(crate) fn matches_identifier(candidate: &T, requested: &[T]) -> bool { + requested.iter().any(|requested| requested == candidate) +} + +/// Returns whether a candidate service set contains one of the requested services. +#[allow(dead_code)] // Used by platform-gated backend implementations. +pub(crate) fn matches_service(candidate_services: &[Uuid], requested: &[Uuid]) -> bool { + requested + .iter() + .any(|requested| candidate_services.contains(requested)) +} + +/// Returns whether a candidate matches either supplied selector. +/// +/// A selector is considered supplied even when empty; in that case it matches nothing. +#[allow(dead_code)] // Used by platform-gated backend implementations. +pub(crate) fn matches_retrieval_selectors( + candidate_id: &PeripheralId, + candidate_services: &[Uuid], + options: &RetrievePeripheralsOptions, +) -> bool { + let id_match = options + .identifiers + .as_deref() + .is_some_and(|requested| matches_identifier(candidate_id, requested)); + let service_match = options + .services + .as_deref() + .is_some_and(|requested| matches_service(candidate_services, requested)); + + if options.identifiers.is_none() && options.services.is_none() { + true + } else { + id_match || service_match + } +} + +/// Merges retrieved peripherals while preserving the first occurrence of each identifier. +#[allow(dead_code)] // Used by platform-gated backend implementations. +pub(crate) fn merge_retrieved_peripherals( + peripherals: impl IntoIterator, + id: F, +) -> Vec

+where + K: Eq + Hash, + F: Fn(&P) -> K, +{ + let mut seen = HashSet::new(); + peripherals + .into_iter() + .filter(|peripheral| seen.insert(id(peripheral))) + .collect() +} + +#[cfg(test)] +fn unsupported_retrieve_peripherals

() -> Result> { + Err(crate::Error::NotSupported( + "retrieve_peripherals".to_string(), + )) +} + /// Current BLE connection parameters as reported by the OS. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ConnectionParameters { @@ -263,8 +343,13 @@ pub trait Peripheral: Send + Sync + Clone + Debug { /// Returns the currently negotiated mtu size fn mtu(&self) -> u16; - /// Returns the set of properties associated with the peripheral. These may be updated over time - /// as additional advertising reports are received. + /// Returns the properties currently known for the peripheral. + /// + /// `Ok(Some(_))` contains a snapshot of the properties available to the backend. The snapshot + /// may be updated as additional advertising reports are received, and individual fields may be + /// unavailable (`None`) when the peripheral has not advertised them or the platform does not + /// expose them. `Ok(None)` means that the backend has no properties snapshot available yet; + /// callers should handle this case rather than assuming that properties are always available. async fn properties(&self) -> Result>; /// The set of services we've discovered for this device. This will be empty until @@ -457,6 +542,22 @@ pub trait Central: Send + Sync + Clone { /// may contain peripherals that are no longer available. async fn peripherals(&self) -> Result>; + /// Retrieves peripherals from the backend's connected-device or known-device source. + /// + /// Selectors are combined as a union: a peripheral is returned when its identifier matches + /// any requested identifier or its backend-reported services contain any requested service. + /// Results are in backend order and deduplicated by [`Peripheral::id`]. An explicitly empty + /// selector matches nothing. Backends without a retrieval source return + /// [`Error::NotSupported`](crate::Error::NotSupported) with `"retrieve_peripherals"`. + async fn retrieve_peripherals( + &self, + _options: RetrievePeripheralsOptions, + ) -> Result> { + Err(crate::Error::NotSupported( + "retrieve_peripherals".to_string(), + )) + } + /// Returns a particular [`Peripheral`] by its address if it has been discovered. async fn peripheral(&self, id: &PeripheralId) -> Result; @@ -474,11 +575,106 @@ pub trait Central: Send + Sync + Clone { /// be useful for debug logs. async fn adapter_info(&self) -> Result; + /// Retrieve the Bluetooth address exposed by the local adapter, when available. + /// + /// `Ok(Some(address))` means the platform exposed a usable adapter Bluetooth address. + /// `Ok(None)` means the platform or its public API does not expose one. `Err` means + /// retrieving the adapter metadata failed operationally. This value is optional and is + /// not the portable adapter identity: callers should continue using the platform adapter + /// handle and [`PeripheralId`] for identity and lookup. + /// + /// ```no_run + /// # use btleplug::api::Central as _; + /// # async fn example(adapter: impl btleplug::api::Central) { + /// match adapter.adapter_address().await { + /// Ok(Some(address)) => println!("adapter address: {address}"), + /// Ok(None) => println!("adapter address is unavailable on this platform"), + /// Err(error) => eprintln!("could not query adapter address: {error}"), + /// } + /// # } + /// ``` + async fn adapter_address(&self) -> Result> { + Ok(None) + } + /// Get information about the Bluetooth adapter state. async fn adapter_state(&self) -> Result; } -/// The Manager is the entry point to the library, providing access to all the Bluetooth adapters on +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn retrieve_options_are_publicly_constructible() { + let options = RetrievePeripheralsOptions { + identifiers: Some(Vec::new()), + services: Some(vec![Uuid::nil()]), + }; + assert_eq!(options.services, Some(vec![Uuid::nil()])); + assert_eq!(options.identifiers, Some(Vec::new())); + } + + #[test] + fn retrieve_options_default_is_explicit() { + assert_eq!(RetrievePeripheralsOptions::default().identifiers, None); + assert_eq!(RetrievePeripheralsOptions::default().services, None); + } + + #[test] + fn retrieve_empty_identifier_selector_matches_nothing() { + assert!(!matches_identifier(&1_u8, &[])); + } + + #[test] + fn retrieve_empty_service_selector_matches_nothing() { + assert!(!matches_service(&[Uuid::nil()], &[])); + } + + #[test] + fn retrieve_selector_matching_uses_any_value() { + assert!(matches_identifier(&2_u8, &[1, 2, 3])); + assert!(matches_service( + &[Uuid::nil()], + &[Uuid::from_u128(1), Uuid::nil()], + )); + } + + #[test] + fn retrieve_unknown_identifiers_are_omitted() { + let requested = [1_u8, 2_u8]; + assert!(!matches_identifier(&3, &requested)); + } + + #[test] + fn retrieve_order_is_preserved() { + let merged = merge_retrieved_peripherals([3_u8, 1, 2], |value| *value); + assert_eq!(merged, vec![3, 1, 2]); + } + + #[test] + fn retrieve_combined_selectors_use_union() { + assert!(matches_service(&[Uuid::nil()], &[Uuid::nil()])); + assert!(!matches_service(&[], &[Uuid::nil()])); + } + + #[test] + fn retrieve_results_are_deduplicated() { + let merged = merge_retrieved_peripherals([1_u8, 2, 1, 3, 2], |value| *value); + assert_eq!(merged, vec![1, 2, 3]); + } + + #[test] + fn retrieve_peripherals_default_is_not_supported() { + let error = unsupported_retrieve_peripherals::().unwrap_err(); + assert!(matches!( + error, + crate::Error::NotSupported(operation) if operation == "retrieve_peripherals" + )); + } +} + +/// The Manager is the entry point for the library, providing access to all Bluetooth adapters on /// the system. You can obtain an instance from [`platform::Manager::new()`](crate::platform::Manager::new). /// /// ## Usage @@ -504,3 +700,31 @@ pub trait Manager { /// Get a list of all Bluetooth adapters on the system. Each adapter implements [`Central`]. async fn adapters(&self) -> Result>; } + +#[cfg(all(test, feature = "serde"))] +mod serde_tests { + use super::PeripheralProperties; + + #[test] + fn peripheral_properties_round_trip_appearance() { + let properties = PeripheralProperties { + appearance: Some(0x0340), + ..PeripheralProperties::default() + }; + + let value = serde_json::to_value(&properties).unwrap(); + assert_eq!(value["appearance"], 0x0340); + + let decoded: PeripheralProperties = serde_json::from_value(value).unwrap(); + assert_eq!(decoded.appearance, Some(0x0340)); + } + + #[test] + fn peripheral_properties_missing_appearance_defaults_to_none() { + let mut value = serde_json::to_value(PeripheralProperties::default()).unwrap(); + value.as_object_mut().unwrap().remove("appearance").unwrap(); + + let properties: PeripheralProperties = serde_json::from_value(value).unwrap(); + assert_eq!(properties.appearance, None); + } +} diff --git a/src/bluez/adapter.rs b/src/bluez/adapter.rs index e8d495b7..309e28f3 100644 --- a/src/bluez/adapter.rs +++ b/src/bluez/adapter.rs @@ -1,5 +1,7 @@ use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{ + self, BDAddr, Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter, +}; use crate::{Error, Result}; use async_trait::async_trait; use bluez_async::{ @@ -29,6 +31,22 @@ fn get_central_state(powered: bool) -> CentralState { } } +fn matches_retrieval_options( + candidate_id: &bluez_async::DeviceId, + candidate_services: &[uuid::Uuid], + connected: bool, + options: &RetrievePeripheralsOptions, +) -> bool { + let candidate_id = PeripheralId(candidate_id.clone()); + let service_match = connected && options.services.is_some(); + let services = if service_match { + candidate_services + } else { + &[] + }; + api::matches_retrieval_selectors(&candidate_id, services, options) +} + #[async_trait] impl Central for Adapter { type Peripheral = Peripheral; @@ -99,6 +117,22 @@ impl Central for Adapter { .collect()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + let devices = self.session.get_devices_on_adapter(&self.adapter).await?; + let devices = devices.into_iter().filter(|device| { + matches_retrieval_options(&device.id, &device.services, device.connected, &options) + }); + let devices = api::merge_retrieved_peripherals(devices, |device| device.id.clone()); + + Ok(devices + .into_iter() + .map(|device| Peripheral::new(self.session.clone(), device)) + .collect()) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { let device = self.session.get_device_info(&id.0).await.map_err(|e| { if let BluetoothError::DbusError(_) = e { @@ -126,6 +160,16 @@ impl Central for Adapter { Ok(format!("{} ({})", adapter_info.id, adapter_info.modalias)) } + async fn adapter_address(&self) -> Result> { + let address: BDAddr = self + .session + .get_adapter_info(&self.adapter) + .await? + .mac_address + .into(); + Ok((address != BDAddr::default()).then_some(address)) + } + async fn adapter_state(&self) -> Result { let mut powered = false; if let Ok(info) = self.session.get_adapter_info(&self.adapter).await { @@ -206,14 +250,11 @@ async fn central_events( }, BluetoothEvent::Adapter { id, - event: adapter_event, - } if id == adapter_id => match adapter_event { - AdapterEvent::Powered { powered } => { - let state = get_central_state(powered); - Some(vec![CentralEvent::StateUpdate(state)]) - } - _ => None, - }, + event: AdapterEvent::Powered { powered }, + } if id == adapter_id => { + let state = get_central_state(powered); + Some(vec![CentralEvent::StateUpdate(state)]) + } _ => None, } } diff --git a/src/bluez/peripheral.rs b/src/bluez/peripheral.rs index 2310aa1b..c9bed2fe 100644 --- a/src/bluez/peripheral.rs +++ b/src/bluez/peripheral.rs @@ -140,8 +140,8 @@ impl api::Peripheral for Peripheral { fn mtu(&self) -> u16 { let services = self.services.lock().unwrap(); - for (_, service) in services.iter() { - for (_, characteristic) in service.characteristics.iter() { + for service in services.values() { + if let Some((_, characteristic)) = service.characteristics.iter().next() { return characteristic.info.mtu.unwrap(); } } @@ -156,6 +156,7 @@ impl api::Peripheral for Peripheral { address_type: Some(device_info.address_type.into()), local_name: device_info.alias.or(device_info.name.clone()), advertisement_name: device_info.name, + appearance: device_info.appearance, tx_power_level: device_info.tx_power, rssi: device_info.rssi, manufacturer_data: device_info.manufacturer_data, @@ -202,9 +203,7 @@ impl api::Peripheral for Peripheral { // This "should" be unique, but of course it's not enforced HashMap::::new(), |mut map, characteristic| { - if !map.contains_key(&characteristic.uuid) { - map.insert(characteristic.uuid, characteristic); - } + map.entry(characteristic.uuid).or_insert(characteristic); map }, ) @@ -391,8 +390,8 @@ fn make_characteristic( uuid: info.uuid, properties: info.flags.into(), descriptors: descriptors - .iter() - .map(|(_, descriptor)| make_descriptor(descriptor, info.uuid, service_uuid)) + .values() + .map(|descriptor| make_descriptor(descriptor, info.uuid, service_uuid)) .collect(), service_uuid, } diff --git a/src/common/util.rs b/src/common/util.rs index 6f0d3962..47ef43a8 100644 --- a/src/common/util.rs +++ b/src/common/util.rs @@ -11,6 +11,7 @@ use std::pin::Pin; use tokio::sync::broadcast::Receiver; use tokio_stream::wrappers::BroadcastStream; +#[allow(dead_code)] pub fn notifications_stream_from_broadcast_receiver( receiver: Receiver, ) -> Pin + Send>> { diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index 0c489a28..51b01119 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -3,7 +3,10 @@ use super::internal::{ run_corebluetooth_thread, }; use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{ + BDAddr, Central, CentralEvent, CentralState, Peripheral as PeripheralTrait, + RetrievePeripheralsOptions, ScanFilter, +}; use crate::common::adapter_manager::AdapterManager; use crate::{Error, Result}; use async_trait::async_trait; @@ -12,6 +15,7 @@ use futures::sink::SinkExt; use futures::stream::{Stream, StreamExt}; use log::*; use objc2_core_bluetooth::CBManagerState; +use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::task; @@ -53,6 +57,7 @@ impl Adapter { let manager_clone = manager.clone(); let adapter_sender_clone = adapter_sender.clone(); task::spawn(async move { + let mut handles = HashMap::new(); while let Some(msg) = receiver.next().await { match msg { CoreBluetoothEvent::DeviceDiscovered { @@ -61,15 +66,58 @@ impl Adapter { advertisement_name, event_receiver, } => { - manager_clone.add_peripheral(Peripheral::new( - uuid, - local_name, - advertisement_name, - Arc::downgrade(&manager_clone), - event_receiver, - adapter_sender_clone.clone(), - )); - manager_clone.emit(CentralEvent::DeviceDiscovered(uuid.into())); + if manager_clone.peripheral(&uuid.into()).is_none() { + let peripheral = Peripheral::new( + uuid, + local_name, + advertisement_name, + Arc::downgrade(&manager_clone), + event_receiver, + adapter_sender_clone.clone(), + ); + handles.insert(peripheral.id(), peripheral.clone()); + manager_clone.add_peripheral(peripheral); + manager_clone.emit(CentralEvent::DeviceDiscovered(uuid.into())); + } + } + CoreBluetoothEvent::RetrievedPeripherals { + peripherals, + future, + } => { + let mut result = Vec::with_capacity(peripherals.len()); + for retrieved in peripherals { + let id = retrieved.uuid.into(); + let peripheral = if let Some(peripheral) = handles.get(&id).cloned() { + peripheral.update_name( + retrieved.local_name.clone(), + retrieved.advertisement_name.clone(), + ); + peripheral + } else if let Some(event_receiver) = retrieved.event_receiver { + let peripheral = Peripheral::new( + retrieved.uuid, + retrieved.local_name, + retrieved.advertisement_name, + Arc::downgrade(&manager_clone), + event_receiver, + adapter_sender_clone.clone(), + ); + handles.insert(id.clone(), peripheral.clone()); + peripheral + } else { + continue; + }; + + if manager_clone.peripheral(&id).is_none() { + manager_clone.add_peripheral(peripheral.clone()); + manager_clone.emit(CentralEvent::DeviceDiscovered(id)); + } + result.push(peripheral); + } + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Peripherals(result)); } CoreBluetoothEvent::DeviceUpdated { uuid, @@ -83,8 +131,14 @@ impl Adapter { } } CoreBluetoothEvent::DeviceDisconnected { uuid } => { + handles.remove(&uuid.into()); manager_clone.emit(CentralEvent::DeviceDisconnected(uuid.into())); } + CoreBluetoothEvent::PeripheralsCleared { future } => { + manager_clone.clear_peripherals(); + handles.clear(); + future.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + } CoreBluetoothEvent::DidUpdateState { state } => { let central_state = get_central_state(state); manager_clone.emit(CentralEvent::StateUpdate(central_state)); @@ -128,6 +182,36 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + if options.identifiers.is_none() && options.services.is_none() { + return Err(Error::NotSupported("retrieve_peripherals".to_string())); + } + if options.identifiers.as_ref().is_some_and(Vec::is_empty) + && options.services.as_ref().is_none_or(Vec::is_empty) + { + return Ok(Vec::new()); + } + let fut = CoreBluetoothReplyFuture::default(); + self.sender + .to_owned() + .send(CoreBluetoothMessage::RetrievePeripherals { + options, + future: fut.get_state_clone(), + }) + .await?; + match fut.await { + CoreBluetoothReply::Peripherals(peripherals) => Ok(peripherals), + CoreBluetoothReply::Err(msg) => Err(Error::RuntimeError(msg)), + CoreBluetoothReply::Ok => Ok(Vec::new()), + _ => Err(Error::RuntimeError( + "Unexpected CoreBluetooth retrieval reply".to_string(), + )), + } + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } @@ -139,8 +223,20 @@ impl Central for Adapter { } async fn clear_peripherals(&self) -> Result<()> { - self.manager.clear_peripherals(); - Ok(()) + let fut = CoreBluetoothReplyFuture::default(); + self.sender + .to_owned() + .send(CoreBluetoothMessage::ClearPeripherals { + future: fut.get_state_clone(), + }) + .await + .map_err(|e| Error::Other(Box::new(e)))?; + match fut.await { + CoreBluetoothReply::Ok => Ok(()), + _ => Err(Error::RuntimeError( + "Unexpected CoreBluetooth clear reply".to_string(), + )), + } } async fn adapter_info(&self) -> Result { @@ -148,6 +244,11 @@ impl Central for Adapter { Ok("CoreBluetooth".to_string()) } + async fn adapter_address(&self) -> Result> { + // CoreBluetooth exposes opaque UUID identities, not controller addresses. + Ok(None) + } + async fn adapter_state(&self) -> Result { let fut = CoreBluetoothReplyFuture::default(); self.sender diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index eb626c2e..714b15bf 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -20,9 +20,9 @@ use super::utils::nsstring_to_string; use super::utils::{core_bluetooth::cbuuid_to_uuid, nsuuid_to_uuid}; use futures::channel::mpsc::Sender; use futures::sink::SinkExt; -use log::{error, trace}; +use log::{error, trace, warn}; use objc2::runtime::{AnyObject, ProtocolObject}; -use objc2::{ClassType, DeclaredClass, declare_class, msg_send_id, mutability, rc::Retained}; +use objc2::{AnyThread, ClassType, DefinedClass, define_class, msg_send, rc::Retained}; use objc2_core_bluetooth::{ CBAdvertisementDataLocalNameKey, CBAdvertisementDataManufacturerDataKey, CBAdvertisementDataServiceDataKey, CBAdvertisementDataServiceUUIDsKey, @@ -99,22 +99,26 @@ pub enum CentralDelegateEvent { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, CharacteristicUnsubscribed { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, CharacteristicNotified { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, data: Vec, + error: Option, }, CharacteristicWritten { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, DescriptorNotified { peripheral_uuid: Uuid, @@ -122,12 +126,14 @@ pub enum CentralDelegateEvent { characteristic_uuid: Uuid, descriptor_uuid: Uuid, data: Vec, + error: Option, }, DescriptorWritten { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, descriptor_uuid: Uuid, + error: Option, }, TxPowerLevel { peripheral_uuid: Uuid, @@ -136,6 +142,7 @@ pub enum CentralDelegateEvent { DidReadRssi { peripheral_uuid: Uuid, rssi: i16, + error: Option, }, ReadyToSendWriteWithoutResponse { peripheral_uuid: Uuid, @@ -210,6 +217,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicSubscribed") .field("peripheral_uuid", peripheral_uuid) @@ -220,6 +228,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicUnsubscribed") .field("peripheral_uuid", peripheral_uuid) @@ -231,6 +240,7 @@ impl Debug for CentralDelegateEvent { service_uuid, characteristic_uuid, data, + .. } => f .debug_struct("CharacteristicNotified") .field("peripheral_uuid", peripheral_uuid) @@ -242,6 +252,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicWritten") .field("service_uuid", service_uuid) @@ -290,6 +301,7 @@ impl Debug for CentralDelegateEvent { characteristic_uuid, descriptor_uuid, data, + .. } => f .debug_struct("DescriptorNotified") .field("peripheral_uuid", peripheral_uuid) @@ -303,6 +315,7 @@ impl Debug for CentralDelegateEvent { service_uuid, characteristic_uuid, descriptor_uuid, + .. } => f .debug_struct("DescriptorWritten") .field("service_uuid", service_uuid) @@ -321,6 +334,7 @@ impl Debug for CentralDelegateEvent { CentralDelegateEvent::DidReadRssi { peripheral_uuid, rssi, + .. } => f .debug_struct("DidReadRssi") .field("peripheral_uuid", peripheral_uuid) @@ -334,24 +348,17 @@ impl Debug for CentralDelegateEvent { } } -declare_class!( +define_class!( #[derive(Debug)] + #[unsafe(super(NSObject))] + #[thread_kind = AnyThread] + #[ivars = Sender] pub struct CentralDelegate; - unsafe impl ClassType for CentralDelegate { - type Super = NSObject; - type Mutability = mutability::InteriorMutable; - const NAME: &'static str = "BtlePlugCentralManagerDelegate"; - } - - impl DeclaredClass for CentralDelegate { - type Ivars = Sender; - } - unsafe impl NSObjectProtocol for CentralDelegate {} unsafe impl CBCentralManagerDelegate for CentralDelegate { - #[method(centralManagerDidUpdateState:)] + #[unsafe(method(centralManagerDidUpdateState:))] fn delegate_centralmanagerdidupdatestate(&self, central: &CBCentralManager) { trace!("delegate_centralmanagerdidupdatestate"); let state = unsafe { central.state() }; @@ -363,7 +370,7 @@ declare_class!( // trace!("delegate_centralmanager_willrestorestate"); // } - #[method(centralManager:didConnectPeripheral:)] + #[unsafe(method(centralManager:didConnectPeripheral:))] fn delegate_centralmanager_didconnectperipheral( &self, _central: &CBCentralManager, @@ -379,7 +386,7 @@ declare_class!( self.send_event(CentralDelegateEvent::ConnectedDevice { peripheral_uuid }); } - #[method(centralManager:didDisconnectPeripheral:error:)] + #[unsafe(method(centralManager:didDisconnectPeripheral:error:))] fn delegate_centralmanager_diddisconnectperipheral_error( &self, _central: &CBCentralManager, @@ -396,7 +403,7 @@ declare_class!( self.send_event(CentralDelegateEvent::DisconnectedDevice { peripheral_uuid }); } - #[method(centralManager:didFailToConnectPeripheral:error:)] + #[unsafe(method(centralManager:didFailToConnectPeripheral:error:))] fn delegate_centralmanager_didfailtoconnectperipheral_error( &self, _central: &CBCentralManager, @@ -413,7 +420,7 @@ declare_class!( }); } - #[method(centralManager:didDiscoverPeripheral:advertisementData:RSSI:)] + #[unsafe(method(centralManager:didDiscoverPeripheral:advertisementData:RSSI:))] fn delegate_centralmanager_diddiscoverperipheral_advertisementdata_rssi( &self, _central: &CBCentralManager, @@ -427,12 +434,12 @@ declare_class!( ); let advertisement_name = adv_data - .get(unsafe { CBAdvertisementDataLocalNameKey }) - .map(|name| name as *const AnyObject as *const NSString) - .and_then(|name| unsafe { nsstring_to_string(name) }); + .objectForKey(unsafe { CBAdvertisementDataLocalNameKey }) + .and_then(|name| name.downcast::().ok()) + .and_then(|name| unsafe { nsstring_to_string(&*name as *const NSString) }); self.send_event(CentralDelegateEvent::DiscoveredPeripheral { - cbperipheral: peripheral.retain(), + cbperipheral: unsafe { Retained::retain(peripheral as *const _ as *mut _) }.unwrap(), advertisement_name, }); @@ -441,16 +448,14 @@ declare_class!( let id = unsafe { peripheral.identifier() }; let peripheral_uuid = nsuuid_to_uuid(&id); - let manufacturer_data = adv_data.get(unsafe { CBAdvertisementDataManufacturerDataKey }); + let manufacturer_data = adv_data.objectForKey(unsafe { CBAdvertisementDataManufacturerDataKey }); if let Some(manufacturer_data) = manufacturer_data { // SAFETY: manufacturer_data is `NSData` - let manufacturer_data: *const AnyObject = manufacturer_data; - let manufacturer_data: *const NSData = manufacturer_data.cast(); - let manufacturer_data = unsafe { &*manufacturer_data }; + let manufacturer_data = manufacturer_data.downcast::().unwrap(); if manufacturer_data.len() >= 2 { - let (manufacturer_id, manufacturer_data) = - manufacturer_data.bytes().split_at(2); + let manufacturer_data_vec = manufacturer_data.to_vec(); + let (manufacturer_id, manufacturer_data) = manufacturer_data_vec.split_at(2); self.send_event(CentralDelegateEvent::ManufacturerData { peripheral_uuid, @@ -461,17 +466,15 @@ declare_class!( } } - let service_data = adv_data.get(unsafe { CBAdvertisementDataServiceDataKey }); + let service_data = adv_data.objectForKey(unsafe { CBAdvertisementDataServiceDataKey }); if let Some(service_data) = service_data { // SAFETY: service_data is `NSDictionary` - let service_data: *const AnyObject = service_data; - let service_data: *const NSDictionary = service_data.cast(); - let service_data = unsafe { &*service_data }; + let service_data: Retained> = unsafe { Retained::cast_unchecked(service_data) }; let mut result = HashMap::new(); for uuid in service_data.keys() { - let data = &service_data[uuid]; - result.insert(cbuuid_to_uuid(uuid), data.bytes().to_vec()); + let data = service_data.objectForKey(&uuid).unwrap(); + result.insert(cbuuid_to_uuid(&uuid), data.to_vec()); } self.send_event(CentralDelegateEvent::ServiceData { @@ -481,16 +484,14 @@ declare_class!( }); } - let services = adv_data.get(unsafe { CBAdvertisementDataServiceUUIDsKey }); + let services = adv_data.objectForKey(unsafe { CBAdvertisementDataServiceUUIDsKey }); if let Some(services) = services { // SAFETY: services is `NSArray` - let services: *const AnyObject = services; - let services: *const NSArray = services.cast(); - let services = unsafe { &*services }; + let services: Retained> = unsafe { Retained::cast_unchecked(services) }; let mut service_uuids = Vec::new(); for uuid in services { - service_uuids.push(cbuuid_to_uuid(uuid)); + service_uuids.push(cbuuid_to_uuid(&uuid)); } self.send_event(CentralDelegateEvent::Services { @@ -501,11 +502,9 @@ declare_class!( } let tx_power_level = adv_data - .get(unsafe { CBAdvertisementDataTxPowerLevelKey }) + .objectForKey(unsafe { CBAdvertisementDataTxPowerLevelKey }) .map(|val| { - let val: *const AnyObject = val; - let val: *const NSNumber = val.cast(); - unsafe { &*val }.as_i16() + val.downcast::().unwrap().as_i16() }); if let Some(tx_power_level) = tx_power_level { @@ -518,7 +517,7 @@ declare_class!( } unsafe impl CBPeripheralDelegate for CentralDelegate { - #[method(peripheral:didDiscoverServices:)] + #[unsafe(method(peripheral:didDiscoverServices:))] fn delegate_peripheral_diddiscoverservices( &self, peripheral: &CBPeripheral, @@ -553,7 +552,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverIncludedServicesForService:error:)] + #[unsafe(method(peripheral:didDiscoverIncludedServicesForService:error:))] fn delegate_peripheral_diddiscoverincludedservicesforservice_error( &self, peripheral: &CBPeripheral, @@ -574,7 +573,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverCharacteristicsForService:error:)] + #[unsafe(method(peripheral:didDiscoverCharacteristicsForService:error:))] fn delegate_peripheral_diddiscovercharacteristicsforservice_error( &self, peripheral: &CBPeripheral, @@ -609,7 +608,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverDescriptorsForCharacteristic:error:)] + #[unsafe(method(peripheral:didDiscoverDescriptorsForCharacteristic:error:))] fn delegate_peripheral_diddiscoverdescriptorsforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -622,32 +621,46 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); + // Send the event even on error when the characteristic is associated + // with a service, so discover_services() can complete. + let mut descriptors = HashMap::new(); + if error.is_some() { + warn!( + "Error discovering descriptors for characteristic {}, continuing with empty descriptors: {}", + characteristic_debug(characteristic), + localized_description(error) + ); + } if error.is_none() { - let mut descriptors = HashMap::new(); let descs = unsafe { characteristic.descriptors() }.unwrap_or_default(); for d in descs { - // Create the map entry we'll need to export. let raw_uuid = unsafe { d.UUID() }; let uuid = cbuuid_to_uuid(&raw_uuid); descriptors.insert(uuid, d); } - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let service = unsafe { characteristic.service() }.unwrap(); - let raw_service_uuid = unsafe { service.UUID() }; - let service_uuid = cbuuid_to_uuid(&raw_service_uuid); - let raw_char_uuid = unsafe { characteristic.UUID() }; - let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); - self.send_event(CentralDelegateEvent::DiscoveredCharacteristicDescriptors { - peripheral_uuid, - service_uuid, - characteristic_uuid, - descriptors, - }); } + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let Some(service) = (unsafe { characteristic.service() }) else { + warn!( + "Descriptor discovery completed for characteristic {} without an associated service", + characteristic_debug(characteristic) + ); + return; + }; + let raw_service_uuid = unsafe { service.UUID() }; + let service_uuid = cbuuid_to_uuid(&raw_service_uuid); + let raw_char_uuid = unsafe { characteristic.UUID() }; + let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); + self.send_event(CentralDelegateEvent::DiscoveredCharacteristicDescriptors { + peripheral_uuid, + service_uuid, + characteristic_uuid, + descriptors, + }); } - #[method(peripheral:didUpdateValueForCharacteristic:error:)] + #[unsafe(method(peripheral:didUpdateValueForCharacteristic:error:))] fn delegate_peripheral_didupdatevalueforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -660,25 +673,29 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); - if error.is_none() { - let service = unsafe { characteristic.service() }.unwrap(); - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let raw_service_uuid = unsafe { service.UUID() }; - let service_uuid = cbuuid_to_uuid(&raw_service_uuid); - let raw_char_uuid = unsafe { characteristic.UUID() }; - let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); - self.send_event(CentralDelegateEvent::CharacteristicNotified { - peripheral_uuid, - service_uuid, - characteristic_uuid, - data: get_characteristic_value(characteristic), - }); - // Notify BluetoothGATTCharacteristic::read_value that read was successful. - } + let Some(service) = (unsafe { characteristic.service() }) else { + warn!( + "Characteristic value update for {} has no associated service", + characteristic_debug(characteristic) + ); + return; + }; + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let raw_service_uuid = unsafe { service.UUID() }; + let service_uuid = cbuuid_to_uuid(&raw_service_uuid); + let raw_char_uuid = unsafe { characteristic.UUID() }; + let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); + self.send_event(CentralDelegateEvent::CharacteristicNotified { + peripheral_uuid, + service_uuid, + characteristic_uuid, + data: get_characteristic_value(characteristic), + error: error.map(|e| e.localizedDescription().to_string()), + }); } - #[method(peripheral:didWriteValueForCharacteristic:error:)] + #[unsafe(method(peripheral:didWriteValueForCharacteristic:error:))] fn delegate_peripheral_didwritevalueforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -691,7 +708,15 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); - if error.is_none() { + if error.is_some() { + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::CharacteristicWritten { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + error: error.map(|e| e.localizedDescription().to_string()), + }); + } else { let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; let peripheral_uuid = nsuuid_to_uuid(&id); @@ -703,16 +728,17 @@ declare_class!( peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } - #[method(peripheral:didUpdateNotificationStateForCharacteristic:error:)] + #[unsafe(method(peripheral:didUpdateNotificationStateForCharacteristic:error:))] fn delegate_peripheral_didupdatenotificationstateforcharacteristic_error( &self, peripheral: &CBPeripheral, characteristic: &CBCharacteristic, - _error: Option<&NSError>, + error: Option<&NSError>, ) { trace!("delegate_peripheral_didupdatenotificationstateforcharacteristic_error"); // TODO check for error here @@ -728,17 +754,19 @@ declare_class!( peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } else { self.send_event(CentralDelegateEvent::CharacteristicUnsubscribed { peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } - #[method(peripheral:didReadRSSI:error:)] + #[unsafe(method(peripheral:didReadRSSI:error:))] fn delegate_peripheral_didreadrssi_error( &self, peripheral: &CBPeripheral, @@ -749,18 +777,17 @@ declare_class!( "delegate_peripheral_didreadrssi_error {}", peripheral_debug(peripheral) ); - if error.is_none() { - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let rssi_value = rssi.as_i16(); - self.send_event(CentralDelegateEvent::DidReadRssi { - peripheral_uuid, - rssi: rssi_value, - }); - } + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let rssi_value = rssi.as_i16(); + self.send_event(CentralDelegateEvent::DidReadRssi { + peripheral_uuid, + rssi: rssi_value, + error: error.map(|e| e.localizedDescription().to_string()), + }); } - #[method(peripheral:didUpdateValueForDescriptor:error:)] + #[unsafe(method(peripheral:didUpdateValueForDescriptor:error:))] fn delegate_peripheral_didupdatevaluefordescriptor_error( &self, peripheral: &CBPeripheral, @@ -773,7 +800,17 @@ declare_class!( descriptor_debug(descriptor), localized_description(error) ); - if error.is_none() { + if let Some(error) = error { + let Some(characteristic) = (unsafe { descriptor.characteristic() }) else { return }; + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::DescriptorNotified { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + descriptor_uuid: cbuuid_to_uuid(&*unsafe { descriptor.UUID() }), + data: Vec::new(), error: Some(error.localizedDescription().to_string()), + }); + } else { let characteristic = unsafe { descriptor.characteristic() }.unwrap(); let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; @@ -789,13 +826,14 @@ declare_class!( service_uuid, characteristic_uuid, descriptor_uuid, - data: get_descriptor_value(&descriptor), + data: get_descriptor_value(descriptor), + error: error.map(|e| e.localizedDescription().to_string()), }); // Notify BluetoothGATTCharacteristic::read_value that read was successful. } } - #[method(peripheral:didWriteValueForDescriptor:error:)] + #[unsafe(method(peripheral:didWriteValueForDescriptor:error:))] fn delegate_peripheral_didwritevaluefordescriptor_error( &self, peripheral: &CBPeripheral, @@ -808,7 +846,17 @@ declare_class!( descriptor_debug(descriptor), localized_description(error) ); - if error.is_none() { + if let Some(error) = error { + let Some(characteristic) = (unsafe { descriptor.characteristic() }) else { return }; + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::DescriptorWritten { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + descriptor_uuid: cbuuid_to_uuid(&*unsafe { descriptor.UUID() }), + error: Some(error.localizedDescription().to_string()), + }); + } else { let characteristic = unsafe { descriptor.characteristic() }.unwrap(); let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; @@ -824,11 +872,12 @@ declare_class!( service_uuid, characteristic_uuid, descriptor_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } - #[method(peripheral:didModifyServices:)] + #[unsafe(method(peripheral:didModifyServices:))] fn delegate_peripheral_didmodifyservices( &self, peripheral: &CBPeripheral, @@ -850,7 +899,7 @@ declare_class!( }); } - #[method(peripheralIsReadyToSendWriteWithoutResponse:)] + #[unsafe(method(peripheralIsReadyToSendWriteWithoutResponse:))] fn delegate_peripheral_is_ready_to_send_write_without_response( &self, peripheral: &CBPeripheral, @@ -871,7 +920,7 @@ declare_class!( impl CentralDelegate { pub fn new(sender: Sender) -> Retained { let this = CentralDelegate::alloc().set_ivars(sender); - unsafe { msg_send_id![super(this), init] } + unsafe { msg_send![super(this), init] } } fn send_event(&self, event: CentralDelegateEvent) { @@ -894,14 +943,14 @@ fn localized_description(error: Option<&NSError>) -> String { fn get_characteristic_value(characteristic: &CBCharacteristic) -> Vec { trace!("Getting data!"); - let v = unsafe { characteristic.value() }.map(|value| value.bytes().into()); + let v = unsafe { characteristic.value() }.map(|value| value.to_vec()); trace!("BluetoothGATTCharacteristic::get_value -> {:?}", v); v.unwrap_or_default() } fn get_descriptor_value(descriptor: &CBDescriptor) -> Vec { trace!("Getting data!"); - let v = unsafe { descriptor.value() }.map(|value| unsafe { + let v = unsafe { descriptor.value() }.map(|value| { let mut clazz = value.class(); // Find the root class until we reach NSObject while let Some(superclass) = clazz.superclass() { @@ -911,17 +960,17 @@ fn get_descriptor_value(descriptor: &CBDescriptor) -> Vec { clazz = superclass; } - match clazz.name() { - "NSString" => { - let d: Retained = Retained::cast(value); + match clazz.name().to_bytes() { + b"NSString" => { + let d: Retained = value.downcast().unwrap(); d.to_string().into_bytes() } - "NSData" => { - let d: Retained = Retained::cast(value); - d.bytes().into() + b"NSData" => { + let d: Retained = value.downcast().unwrap(); + d.to_vec() } - "NSNumber" => { - let d: Retained = Retained::cast(value); + b"NSNumber" => { + let d: Retained = value.downcast().unwrap(); d.stringValue().to_string().into_bytes() } _ => { diff --git a/src/corebluetooth/future.rs b/src/corebluetooth/future.rs index 5757121e..1ab06df9 100644 --- a/src/corebluetooth/future.rs +++ b/src/corebluetooth/future.rs @@ -13,6 +13,7 @@ use std::task::{Context, Poll, Waker}; pub struct BtlePlugFutureState { reply_msg: Option, waker: Option, + completed: bool, } // For some reason, deriving default above doesn't work, but doing an explicit @@ -22,6 +23,7 @@ impl Default for BtlePlugFutureState { BtlePlugFutureState:: { reply_msg: None, waker: None, + completed: false, } } } @@ -36,17 +38,21 @@ impl BtlePlugFutureState { /// # Parameters /// /// - `msg`: Message to set as reply, which will be returned by the - /// corresponding future. + /// corresponding future. pub fn set_reply(&mut self, reply: T) { - if self.reply_msg.is_some() { - // TODO Can we stop multiple calls to set_reply_msg at compile time? - panic!("set_reply_msg called multiple times on the same future."); + // CoreBluetooth can deliver a late callback after a disconnect has + // already completed and drained the operation. Completion is + // terminal, so duplicate callbacks must be harmless (including after + // the reply has been polled by the caller). + if self.completed { + return; } + self.completed = true; self.reply_msg = Some(reply); - if self.waker.is_some() { - self.waker.take().unwrap().wake(); + if let Some(waker) = self.waker.take() { + waker.wake(); } } } @@ -112,3 +118,28 @@ impl Future for BtlePlugFuture { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::task::{Context, Poll, Waker}; + + #[test] + fn late_duplicate_completion_after_poll_is_ignored() { + let mut future = BtlePlugFuture::::default(); + let state = future.get_state_clone(); + let waker = Waker::noop(); + let mut context = Context::from_waker(waker); + + state.lock().unwrap().set_reply(1); + assert_eq!(Pin::new(&mut future).poll(&mut context), Poll::Ready(1)); + + // A callback arriving after the reply was consumed must not resurrect + // the operation or replace its terminal result. + state.lock().unwrap().set_reply(2); + assert!(matches!( + Pin::new(&mut future).poll(&mut context), + Poll::Pending + )); + } +} diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index c0f42aa6..bebf2a72 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -12,26 +12,30 @@ use super::{ central_delegate::{CentralDelegate, CentralDelegateEvent}, ffi, future::{BtlePlugFuture, BtlePlugFutureStateShared}, + peripheral::Peripheral, utils::{ core_bluetooth::{cbuuid_to_uuid, uuid_to_cbuuid}, nsuuid_to_uuid, }, }; use crate::Error; -use crate::api::{CharPropFlags, Characteristic, Descriptor, ScanFilter, Service, WriteType}; +use crate::api::{ + CharPropFlags, Characteristic, Descriptor, RetrievePeripheralsOptions, ScanFilter, Service, + WriteType, +}; use futures::channel::mpsc::{self, Receiver, Sender}; use futures::select; use futures::sink::SinkExt; use futures::stream::{Fuse, StreamExt}; -use log::{error, trace, warn}; -use objc2::{ClassType, msg_send_id}; +use log::{debug, error, trace, warn}; +use objc2::{AnyThread, msg_send}; use objc2::{rc::Retained, runtime::AnyObject}; use objc2_core_bluetooth::{ CBCentralManager, CBCentralManagerScanOptionAllowDuplicatesKey, CBCharacteristic, CBCharacteristicProperties, CBCharacteristicWriteType, CBDescriptor, CBManager, CBManagerAuthorization, CBManagerState, CBPeripheral, CBPeripheralState, CBService, CBUUID, }; -use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber}; +use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber, NSString, NSUUID}; use std::{ collections::{BTreeSet, HashMap, VecDeque}, ffi::CString, @@ -42,6 +46,25 @@ use std::{ use tokio::runtime; use uuid::Uuid; +/// ATT Write Command PDUs reserve one byte for the opcode and two bytes for +/// the attribute handle (Bluetooth Core Specification, Vol 3, Part F, 3.4.5.3). +const ATT_WRITE_COMMAND_HEADER_LEN: usize = 3; + +fn maximum_write_value_length_to_att_mtu(maximum_write_value_length: usize) -> Result { + if maximum_write_value_length == 0 { + return Ok(crate::api::DEFAULT_MTU_SIZE); + } + + maximum_write_value_length + .checked_add(ATT_WRITE_COMMAND_HEADER_LEN) + .and_then(|mtu| u16::try_from(mtu).ok()) + .ok_or_else(|| { + format!( + "CoreBluetooth maximum write value length {maximum_write_value_length} cannot be represented as a u16 ATT MTU" + ) + }) +} + struct DescriptorInternal { pub descriptor: Retained, pub uuid: Uuid, @@ -90,7 +113,7 @@ impl Debug for CharacteristicInternal { impl CharacteristicInternal { pub fn new(characteristic: Retained) -> Self { - let properties = CharacteristicInternal::form_flags(&*characteristic); + let properties = CharacteristicInternal::form_flags(&characteristic); let raw_uuid = unsafe { characteristic.UUID() }; let uuid = cbuuid_to_uuid(&raw_uuid); let descriptors_arr = unsafe { characteristic.descriptors() }; @@ -117,28 +140,25 @@ impl CharacteristicInternal { fn form_flags(characteristic: &CBCharacteristic) -> CharPropFlags { let flags = unsafe { characteristic.properties() }; let mut v = CharPropFlags::default(); - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyBroadcast) { + if flags.contains(CBCharacteristicProperties::Broadcast) { v |= CharPropFlags::BROADCAST; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyRead) { + if flags.contains(CBCharacteristicProperties::Read) { v |= CharPropFlags::READ; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyWriteWithoutResponse) - { + if flags.contains(CBCharacteristicProperties::WriteWithoutResponse) { v |= CharPropFlags::WRITE_WITHOUT_RESPONSE; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyWrite) { + if flags.contains(CBCharacteristicProperties::Write) { v |= CharPropFlags::WRITE; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyNotify) { + if flags.contains(CBCharacteristicProperties::Notify) { v |= CharPropFlags::NOTIFY; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyIndicate) { + if flags.contains(CBCharacteristicProperties::Indicate) { v |= CharPropFlags::INDICATE; } - if flags - .contains(CBCharacteristicProperties::CBCharacteristicPropertyAuthenticatedSignedWrites) - { + if flags.contains(CBCharacteristicProperties::AuthenticatedSignedWrites) { v |= CharPropFlags::AUTHENTICATED_SIGNED_WRITES; } trace!("Flags: {:?}", v); @@ -159,9 +179,10 @@ pub enum CoreBluetoothReply { ReadResult(Vec), ReadRssi(i16), Connected, - ServicesDiscovered(BTreeSet), + ServicesDiscovered(BTreeSet, u16), State(CBPeripheralState), Ok, + Peripherals(Vec), Err(String), } @@ -251,7 +272,7 @@ impl PeripheralInternal { // in-flight future state and already-discovered descriptors to // avoid dropping pending operations during late re-discovery // events (see issue #167). - existing.properties = CharacteristicInternal::form_flags(&*cb_characteristic); + existing.properties = CharacteristicInternal::form_flags(&cb_characteristic); existing.characteristic = cb_characteristic; } else { service.characteristics.insert( @@ -271,15 +292,13 @@ impl PeripheralInternal { service_uuid: Uuid, characteristic_uuid: Uuid, descriptors: HashMap>, - ) { - let service = self - .services - .get_mut(&service_uuid) - .expect("Got descriptors for a service we don't know about"); - let characteristic = service - .characteristics - .get_mut(&characteristic_uuid) - .expect("Got descriptors for a characteristic we don't know about"); + ) -> bool { + let Some(service) = self.services.get_mut(&service_uuid) else { + return false; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + return false; + }; for (descriptor_uuid, cb_descriptor) in descriptors { if let Some(existing) = characteristic.descriptors.get_mut(&descriptor_uuid) { // Update the CB object reference but preserve in-flight future @@ -302,6 +321,7 @@ impl PeripheralInternal { service.discovered = true; self.check_discovered() } + true } fn check_discovered(&mut self) { @@ -346,57 +366,75 @@ impl PeripheralInternal { .collect(), }) .collect(); + // CoreBluetooth exposes the maximum characteristic value length for + // a write, not the ATT MTU. Sample it after discovery, then account + // for the ATT Write Command header to infer the full ATT MTU. + let maximum_write_value_length = unsafe { + self.peripheral + .maximumWriteValueLengthForType(CBCharacteristicWriteType::WithoutResponse) + }; + let reply = match maximum_write_value_length_to_att_mtu(maximum_write_value_length) { + Ok(mtu) => CoreBluetoothReply::ServicesDiscovered(services, mtu), + Err(error) => CoreBluetoothReply::Err(error), + }; self.services_discovered_future_state .take() .unwrap() .lock() .unwrap() - .set_reply(CoreBluetoothReply::ServicesDiscovered(services)); + .set_reply(reply); } } pub fn confirm_disconnect(&mut self) { - // Fulfill the disconnected future, if there is one. - // There might not be a future if the device disconnects unexpectedly. - if let Some(future) = self.disconnected_future_state.take() { - future.lock().unwrap().set_reply(CoreBluetoothReply::Ok) - } + self.drain_pending_operations("Device disconnected"); + } - // Fulfill pending RSSI futures - let error = CoreBluetoothReply::Err(String::from("Device disconnected")); + /// Complete every operation that cannot receive a callback after the + /// peripheral disappears. Keep this centralized: adding a future-bearing + /// operation must also add its queue here. + fn drain_pending_operations(&mut self, message: &str) { + let error = CoreBluetoothReply::Err(message.to_string()); + for future in [ + self.disconnected_future_state.take(), + self.connected_future_state.take(), + self.services_discovered_future_state.take(), + ] + .into_iter() + .flatten() + { + future.lock().unwrap().set_reply(error.clone()); + } for state in self.read_rssi_future_state.drain(..) { state.lock().unwrap().set_reply(error.clone()); } - - // Fulfill pending write-without-response futures for pending in self.write_without_response_queue.drain(..) { pending.fut.lock().unwrap().set_reply(error.clone()); } - - // Fulfill all pending futures - self.services.iter().for_each(|(_, service)| { - service - .characteristics - .iter() - .for_each(|(_, characteristic)| { - let CharacteristicInternal { - read_future_state, - write_future_state, - subscribe_future_state, - unsubscribe_future_state, - .. - } = characteristic; - - let futures = read_future_state - .into_iter() - .chain(write_future_state.into_iter()) - .chain(subscribe_future_state.into_iter()) - .chain(unsubscribe_future_state.into_iter()); - for state in futures { + for service in self.services.values_mut() { + for characteristic in service.characteristics.values_mut() { + for queue in [ + &mut characteristic.read_future_state, + &mut characteristic.write_future_state, + &mut characteristic.subscribe_future_state, + &mut characteristic.unsubscribe_future_state, + ] { + for state in queue.drain(..) { state.lock().unwrap().set_reply(error.clone()); } - }); - }); + } + for descriptor in characteristic.descriptors.values_mut() { + for queue in [ + &mut descriptor.read_future_state, + &mut descriptor.write_future_state, + ] { + for state in queue.drain(..) { + state.lock().unwrap().set_reply(error.clone()); + } + } + } + } + } } } @@ -499,6 +537,21 @@ pub enum CoreBluetoothMessage { peripheral_uuid: Uuid, future: CoreBluetoothReplyStateShared, }, + RetrievePeripherals { + options: RetrievePeripheralsOptions, + future: CoreBluetoothReplyStateShared, + }, + ClearPeripherals { + future: CoreBluetoothReplyStateShared, + }, +} + +#[derive(Debug)] +pub struct RetrievedPeripheral { + pub uuid: Uuid, + pub local_name: Option, + pub advertisement_name: Option, + pub event_receiver: Option>, } #[derive(Debug)] @@ -512,6 +565,10 @@ pub enum CoreBluetoothEvent { advertisement_name: Option, event_receiver: Receiver, }, + RetrievedPeripherals { + peripherals: Vec, + future: CoreBluetoothReplyStateShared, + }, DeviceUpdated { uuid: Uuid, local_name: Option, @@ -520,6 +577,9 @@ pub enum CoreBluetoothEvent { DeviceDisconnected { uuid: Uuid, }, + PeripheralsCleared { + future: CoreBluetoothReplyStateShared, + }, } impl CoreBluetoothInternal { @@ -537,7 +597,7 @@ impl CoreBluetoothInternal { let queue: *mut AnyObject = queue.cast(); let manager = unsafe { - msg_send_id![CBCentralManager::alloc(), initWithDelegate: &*delegate, queue: queue] + msg_send![CBCentralManager::alloc(), initWithDelegate: &*delegate, queue: queue] }; Self { @@ -568,18 +628,21 @@ impl CoreBluetoothInternal { "Got manufacturer data advertisement! {}: {:?}", manufacturer_id, manufacturer_data ); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::ManufacturerData( manufacturer_id, manufacturer_data, rssi, )) .await - { - error!("Error sending notification event: {}", e); - } + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } @@ -590,27 +653,33 @@ impl CoreBluetoothInternal { rssi: i16, ) { trace!("Got service data advertisement! {:?}", service_data); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::ServiceData(service_data, rssi)) .await - { - error!("Error sending notification event: {}", e); - } + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } async fn on_services(&mut self, peripheral_uuid: Uuid, services: Vec, rssi: i16) { trace!("Got service advertisement! {:?}", services); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::Services(services, rssi)) .await - { - error!("Error sending notification event: {}", e); - } + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } @@ -639,24 +708,16 @@ impl CoreBluetoothInternal { let id = unsafe { peripheral.identifier() }; let uuid = nsuuid_to_uuid(&id); let peripheral_name = unsafe { peripheral.name() }; - let local_name = peripheral_name - .map(|n| n.to_string()) - .or(advertisement_name.clone()); + // Prefer advertisement_name (from scan response, usually COMPLETE_LOCAL_NAME) + // over peripheral.name() (GAP cache, often the truncated SHORT_LOCAL_NAME) + let local_name = advertisement_name + .clone() + .or_else(|| peripheral_name.map(|n| n.to_string())); - if self.peripherals.contains_key(&uuid) { - if local_name.is_some() || advertisement_name.is_some() { - self.dispatch_event(CoreBluetoothEvent::DeviceUpdated { - uuid, - local_name, - advertisement_name, - }) - .await; - } - } else { + if let std::collections::hash_map::Entry::Vacant(e) = self.peripherals.entry(uuid) { // Create our channels let (event_sender, event_receiver) = mpsc::channel(256); - self.peripherals - .insert(uuid, PeripheralInternal::new(peripheral, event_sender)); + e.insert(PeripheralInternal::new(peripheral, event_sender)); self.dispatch_event(CoreBluetoothEvent::DeviceDiscovered { uuid, local_name, @@ -664,6 +725,15 @@ impl CoreBluetoothInternal { event_receiver, }) .await; + } else { + if local_name.is_some() || advertisement_name.is_some() { + self.dispatch_event(CoreBluetoothEvent::DeviceUpdated { + uuid, + local_name, + advertisement_name, + }) + .await; + } } } @@ -726,8 +796,13 @@ impl CoreBluetoothInternal { for id in descriptors.keys() { trace!("{}", id); } - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors); + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) + && !p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors) + && let Some(future) = p.services_discovered_future_state.take() + { + future.lock().unwrap().set_reply(CoreBluetoothReply::Err( + format!("Unknown descriptor relationship for service {service_uuid}, characteristic {characteristic_uuid}"), + )); } } @@ -737,13 +812,17 @@ impl CoreBluetoothInternal { .peripherals .get_mut(&peripheral_uuid) .expect("If we're here we should have an ID"); - peripheral - .connected_future_state - .take() - .unwrap() - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::Connected); + if let Some(future) = peripheral.connected_future_state.take() { + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Connected); + } else { + debug!( + "Ignoring duplicate connection callback for peripheral {}", + peripheral_uuid + ); + } } } @@ -759,13 +838,17 @@ impl CoreBluetoothInternal { .peripherals .get_mut(&peripheral_uuid) .expect("If we're here we should have an ID"); - peripheral - .connected_future_state - .take() - .unwrap() - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::Err(error)); + if let Some(future) = peripheral.connected_future_state.take() { + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); + } else { + debug!( + "Ignoring duplicate connection failure callback for peripheral {}", + peripheral_uuid + ); + } } } @@ -820,6 +903,14 @@ impl CoreBluetoothInternal { } } + fn complete_missing(fut: CoreBluetoothReplyStateShared, object: &str) { + fut.lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(format!( + "{object} no longer available" + ))); + } + /// Get the CBCharacteristic for the given characteristic of the given peripheral, if it exists. fn get_characteristic( &mut self, @@ -853,13 +944,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got subscribed event!"); - if let Some(state) = characteristic.subscribe_future_state.pop_back() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + if let Some(state) = characteristic.subscribe_future_state.pop_front() { + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -869,13 +964,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got unsubscribed event!"); - if let Some(state) = characteristic.unsubscribe_future_state.pop_back() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + if let Some(state) = characteristic.unsubscribe_future_state.pop_front() { + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -886,39 +985,47 @@ impl CoreBluetoothInternal { service_uuid: Uuid, characteristic_uuid: Uuid, data: Vec, + error: Option, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Got read event!"); - - let mut data_clone = Vec::new(); - for byte in data.iter() { - data_clone.push(*byte); - } - // Reads and notifications both return the same callback. If - // we're trying to do a read, we'll have a future we can - // fulfill. Otherwise, just treat the returned value as a - // notification and use the event system. - if !characteristic.read_future_state.is_empty() { - let state = characteristic.read_future_state.pop_back().unwrap(); - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadResult(data_clone)); - } else if let Err(e) = peripheral - .event_sender - .send(PeripheralEventInternal::Notification( - characteristic_uuid, - service_uuid, - data, - )) - .await - { - error!("Error sending notification event: {}", e); - } + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Got read event!"); + if let Some(error) = error { + if let Some(state) = characteristic.read_future_state.pop_front() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); } + return; + } + + let mut data_clone = Vec::new(); + for byte in data.iter() { + data_clone.push(*byte); + } + // Reads and notifications both return the same callback. If + // we're trying to do a read, we'll have a future we can + // fulfill. Otherwise, just treat the returned value as a + // notification and use the event system. + if !characteristic.read_future_state.is_empty() { + let state = characteristic.read_future_state.pop_front().unwrap(); + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::ReadResult(data_clone)); + } else if let Err(e) = peripheral + .event_sender + .send(PeripheralEventInternal::Notification( + characteristic_uuid, + service_uuid, + data, + )) + .await + { + error!("Error sending notification event: {}", e); } } } @@ -928,13 +1035,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got written event!"); - if let Some(state) = characteristic.write_future_state.pop_back() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + if let Some(state) = characteristic.write_future_state.pop_front() { + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -945,6 +1056,10 @@ impl CoreBluetoothInternal { trace!("Connecting peripheral!"); p.connected_future_state = Some(fut); unsafe { self.manager.connectPeripheral_options(&p.peripheral, None) }; + } else { + fut.lock().unwrap().set_reply(CoreBluetoothReply::Err( + "Peripheral no longer available".into(), + )); } } @@ -954,6 +1069,8 @@ impl CoreBluetoothInternal { trace!("Disconnecting peripheral!"); p.disconnected_future_state = Some(fut); unsafe { self.manager.cancelPeripheralConnection(&p.peripheral) }; + } else { + fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } @@ -982,45 +1099,52 @@ impl CoreBluetoothInternal { kind: WriteType, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Writing value! With kind {:?}", kind); - match kind { - WriteType::WithoutResponse => { - if unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { - unsafe { - peripheral.peripheral.writeValue_forCharacteristic_type( - &NSData::from_vec(data), - &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, - ); - } - fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); - } else { - trace!("Queueing write-without-response (peripheral not ready)"); - peripheral.write_without_response_queue.push_back( - PendingWriteWithoutResponse { - service_uuid, - characteristic_uuid, - data, - fut, - }, - ); - } - } - WriteType::WithResponse => { - unsafe { - peripheral.peripheral.writeValue_forCharacteristic_type( - &NSData::from_vec(data), - &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithResponse, - ); - } - characteristic.write_future_state.push_front(fut); + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + { + trace!("Writing value! With kind {:?}", kind); + match kind { + WriteType::WithoutResponse => { + if unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { + unsafe { + peripheral.peripheral.writeValue_forCharacteristic_type( + &NSData::from_vec(data), + &characteristic.characteristic, + CBCharacteristicWriteType::WithoutResponse, + ); } + fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + } else { + trace!("Queueing write-without-response (peripheral not ready)"); + peripheral.write_without_response_queue.push_back( + PendingWriteWithoutResponse { + service_uuid, + characteristic_uuid, + data, + fut, + }, + ); + } + } + WriteType::WithResponse => { + unsafe { + peripheral.peripheral.writeValue_forCharacteristic_type( + &NSData::from_vec(data), + &characteristic.characteristic, + CBCharacteristicWriteType::WithResponse, + ); } + characteristic.write_future_state.push_back(fut); } } } @@ -1041,7 +1165,7 @@ impl CoreBluetoothInternal { peripheral.peripheral.writeValue_forCharacteristic_type( &NSData::from_vec(pending.data), &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, + CBCharacteristicWriteType::WithoutResponse, ); } pending @@ -1078,19 +1202,26 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Reading value!"); - unsafe { - peripheral - .peripheral - .readValueForCharacteristic(&characteristic.characteristic); - } - characteristic.read_future_state.push_front(fut); - } + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + { + trace!("Reading value!"); + unsafe { + peripheral + .peripheral + .readValueForCharacteristic(&characteristic.characteristic); } + characteristic.read_future_state.push_back(fut); } } @@ -1101,19 +1232,26 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Setting subscribe!"); - unsafe { - peripheral - .peripheral - .setNotifyValue_forCharacteristic(true, &characteristic.characteristic); - } - characteristic.subscribe_future_state.push_front(fut); - } + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + { + trace!("Setting subscribe!"); + unsafe { + peripheral + .peripheral + .setNotifyValue_forCharacteristic(true, &characteristic.characteristic); } + characteristic.subscribe_future_state.push_back(fut); } } @@ -1124,20 +1262,26 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Setting subscribe!"); - unsafe { - peripheral.peripheral.setNotifyValue_forCharacteristic( - false, - &characteristic.characteristic, - ); - } - characteristic.unsubscribe_future_state.push_front(fut); - } + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + { + trace!("Setting subscribe!"); + unsafe { + peripheral + .peripheral + .setNotifyValue_forCharacteristic(false, &characteristic.characteristic); } + characteristic.unsubscribe_future_state.push_back(fut); } } @@ -1150,22 +1294,30 @@ impl CoreBluetoothInternal { data: Vec, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Writing descriptor value!"); - unsafe { - peripheral.peripheral.writeValue_forDescriptor( - &NSData::from_vec(data), - &descriptor.descriptor, - ); - } - descriptor.write_future_state.push_front(fut); - } - } + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) else { + Self::complete_missing(fut, "Descriptor"); + return; + }; + { + trace!("Writing descriptor value!"); + unsafe { + peripheral + .peripheral + .writeValue_forDescriptor(&NSData::from_vec(data), &descriptor.descriptor); } + descriptor.write_future_state.push_back(fut); } } @@ -1177,42 +1329,55 @@ impl CoreBluetoothInternal { descriptor_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Reading descriptor value!"); - unsafe { - peripheral - .peripheral - .readValueForDescriptor(&descriptor.descriptor); - } - descriptor.read_future_state.push_front(fut); - } - } + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) else { + Self::complete_missing(fut, "Descriptor"); + return; + }; + { + trace!("Reading descriptor value!"); + unsafe { + peripheral + .peripheral + .readValueForDescriptor(&descriptor.descriptor); } + descriptor.read_future_state.push_back(fut); } } fn read_rssi(&mut self, peripheral_uuid: Uuid, fut: CoreBluetoothReplyStateShared) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + { trace!("Reading RSSI!"); unsafe { peripheral.peripheral.readRSSI(); } - peripheral.read_rssi_future_state.push_front(fut); + peripheral.read_rssi_future_state.push_back(fut); } } - async fn on_read_rssi(&mut self, peripheral_uuid: Uuid, rssi: i16) { + async fn on_read_rssi(&mut self, peripheral_uuid: Uuid, rssi: i16, error: Option) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { trace!("Got RSSI read event: {}", rssi); - if let Some(state) = peripheral.read_rssi_future_state.pop_back() { - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadRssi(rssi)); + if let Some(state) = peripheral.read_rssi_future_state.pop_front() { + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::ReadRssi(rssi), + }); } // Also send as a peripheral event for CentralEvent emission if let Err(e) = peripheral @@ -1226,14 +1391,13 @@ impl CoreBluetoothInternal { } async fn on_tx_power_level(&mut self, peripheral_uuid: Uuid, tx_power_level: i16) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = peripheral + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Err(e) = peripheral .event_sender .send(PeripheralEventInternal::TxPowerLevel(tx_power_level)) .await - { - error!("Error sending tx_power_level event: {}", e); - } + { + error!("Error sending tx_power_level event: {}", e); } } @@ -1244,26 +1408,33 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, descriptor_uuid: Uuid, data: Vec, + error: Option, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Got read event!"); - - let mut data_clone = Vec::new(); - for byte in data.iter() { - data_clone.push(*byte); - } - if let Some(state) = descriptor.read_future_state.pop_back() { - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadResult(data_clone)); - } - } + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + { + trace!("Got read event!"); + if let Some(error) = error { + if let Some(state) = descriptor.read_future_state.pop_front() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); } + return; + } + + let mut data_clone = Vec::new(); + for byte in data.iter() { + data_clone.push(*byte); + } + if let Some(state) = descriptor.read_future_state.pop_front() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::ReadResult(data_clone)); } } } @@ -1274,6 +1445,7 @@ impl CoreBluetoothInternal { service_uuid: Uuid, characteristic_uuid: Uuid, descriptor_uuid: Uuid, + error: Option, ) { if let Some(descriptor) = self.get_descriptor( peripheral_uuid, @@ -1282,8 +1454,11 @@ impl CoreBluetoothInternal { descriptor_uuid, ) { trace!("Got written event!"); - if let Some(state) = descriptor.write_future_state.pop_back() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + if let Some(state) = descriptor.write_future_state.pop_front() { + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -1297,6 +1472,81 @@ impl CoreBluetoothInternal { } } + async fn retrieve_peripherals( + &mut self, + options: RetrievePeripheralsOptions, + future: CoreBluetoothReplyStateShared, + ) { + if options.identifiers.is_none() && options.services.is_none() { + future.lock().unwrap().set_reply(CoreBluetoothReply::Err( + "retrieve_peripherals requires an identifier or service selector".to_string(), + )); + return; + } + let mut retrieved = Vec::new(); + if let Some(services) = options.services.filter(|services| !services.is_empty()) { + let services = NSArray::from_retained_slice( + &services.into_iter().map(uuid_to_cbuuid).collect::>(), + ); + retrieved.extend(unsafe { + self.manager + .retrieveConnectedPeripheralsWithServices(&services) + }); + } + if let Some(identifiers) = options + .identifiers + .filter(|identifiers| !identifiers.is_empty()) + { + let identifiers = NSArray::from_retained_slice( + &identifiers + .into_iter() + .map(|id| { + NSUUID::from_string(&objc2_foundation::NSString::from_str(&id.to_string())) + .unwrap() + }) + .collect::>(), + ); + retrieved.extend(unsafe { + self.manager + .retrievePeripheralsWithIdentifiers(&identifiers) + }); + } + let mut peripherals = Vec::new(); + for peripheral in retrieved { + let identifier = unsafe { peripheral.identifier() }; + let uuid = nsuuid_to_uuid(&identifier); + if peripherals + .iter() + .any(|retrieved: &RetrievedPeripheral| retrieved.uuid == uuid) + { + continue; + } + + let peripheral_name = unsafe { peripheral.name() }; + let local_name = peripheral_name.map(|name| name.to_string()); + let event_receiver = if let Some(existing) = self.peripherals.get_mut(&uuid) { + existing.peripheral = peripheral; + None + } else { + let (event_sender, event_receiver) = mpsc::channel(256); + self.peripherals + .insert(uuid, PeripheralInternal::new(peripheral, event_sender)); + Some(event_receiver) + }; + peripherals.push(RetrievedPeripheral { + uuid, + local_name, + advertisement_name: None, + event_receiver, + }); + } + self.dispatch_event(CoreBluetoothEvent::RetrievedPeripherals { + peripherals, + future, + }) + .await; + } + async fn wait_for_message(&mut self) { select! { delegate_msg = self.delegate_receiver.select_next_some() => { @@ -1335,23 +1585,27 @@ impl CoreBluetoothInternal { peripheral_uuid, service_uuid, characteristic_uuid, - } => self.on_characteristic_subscribed(peripheral_uuid, service_uuid, characteristic_uuid), + error, + } => self.on_characteristic_subscribed(peripheral_uuid, service_uuid, characteristic_uuid, error), CentralDelegateEvent::CharacteristicUnsubscribed{ peripheral_uuid, service_uuid, characteristic_uuid, - } => self.on_characteristic_unsubscribed(peripheral_uuid, service_uuid,characteristic_uuid), + error, + } => self.on_characteristic_unsubscribed(peripheral_uuid, service_uuid,characteristic_uuid, error), CentralDelegateEvent::CharacteristicNotified{ peripheral_uuid, service_uuid, characteristic_uuid, data, - } => self.on_characteristic_read(peripheral_uuid, service_uuid,characteristic_uuid, data).await, + error, + } => self.on_characteristic_read(peripheral_uuid, service_uuid,characteristic_uuid, data, error).await, CentralDelegateEvent::CharacteristicWritten{ peripheral_uuid, service_uuid, characteristic_uuid, - } => self.on_characteristic_written(peripheral_uuid, service_uuid, characteristic_uuid), + error, + } => self.on_characteristic_written(peripheral_uuid, service_uuid, characteristic_uuid, error), CentralDelegateEvent::ManufacturerData{peripheral_uuid, manufacturer_id, data, rssi} => { self.on_manufacturer_data(peripheral_uuid, manufacturer_id, data, rssi).await }, @@ -1370,18 +1624,20 @@ impl CoreBluetoothInternal { characteristic_uuid, descriptor_uuid, data, - } => self.on_descriptor_read(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, data).await, + error, + } => self.on_descriptor_read(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, data, error).await, CentralDelegateEvent::DescriptorWritten{ peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, - } => self.on_descriptor_written(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid), + error, + } => self.on_descriptor_written(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, error), CentralDelegateEvent::TxPowerLevel{peripheral_uuid, tx_power_level} => { self.on_tx_power_level(peripheral_uuid, tx_power_level).await }, - CentralDelegateEvent::DidReadRssi{peripheral_uuid, rssi} => { - self.on_read_rssi(peripheral_uuid, rssi).await + CentralDelegateEvent::DidReadRssi{peripheral_uuid, rssi, error} => { + self.on_read_rssi(peripheral_uuid, rssi, error).await }, CentralDelegateEvent::ReadyToSendWriteWithoutResponse{peripheral_uuid} => { self.drain_write_without_response_queue(peripheral_uuid) @@ -1438,6 +1694,14 @@ impl CoreBluetoothInternal { CoreBluetoothMessage::ReadRssi{peripheral_uuid, future} => { self.read_rssi(peripheral_uuid, future) } + CoreBluetoothMessage::RetrievePeripherals { options, future } => { + self.retrieve_peripherals(options, future).await + } + CoreBluetoothMessage::ClearPeripherals { future } => { + self.peripherals.clear(); + self.dispatch_event(CoreBluetoothEvent::PeripheralsCleared { future }) + .await; + } }; } } @@ -1453,18 +1717,19 @@ impl CoreBluetoothInternal { fn start_discovery(&mut self, filter: ScanFilter) { trace!("BluetoothAdapter::start_discovery"); let service_uuids = scan_filter_to_service_uuids(filter); - let mut options = NSMutableDictionary::new(); + let options: Retained> = + NSMutableDictionary::new(); // NOTE: If duplicates are not allowed then a peripheral will not show // up again once connected and then disconnected. - options.insert_id( + options.insert( unsafe { CBCentralManagerScanOptionAllowDuplicatesKey }, - Retained::into_super(Retained::into_super(Retained::into_super( - NSNumber::new_bool(true), - ))), + &*Retained::into_super(Retained::into_super(NSNumber::new_bool(true))), ); unsafe { - self.manager - .scanForPeripheralsWithServices_options(service_uuids.as_deref(), Some(&options)) + self.manager.scanForPeripheralsWithServices_options( + service_uuids.as_deref(), + Some(&*Retained::into_super(options)), + ) }; } @@ -1485,7 +1750,7 @@ fn scan_filter_to_service_uuids(filter: ScanFilter) -> Option>(); - Some(NSArray::from_vec(service_uuids)) + Some(NSArray::from_retained_slice(&service_uuids)) } } @@ -1497,6 +1762,183 @@ impl Drop for CoreBluetoothInternal { } } +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use objc2::{DefinedClass, define_class}; + use objc2_core_bluetooth::{ + CBAttributePermissions, CBMutableCharacteristic, CBMutableService, CBPeripheralDelegate, + }; + use objc2_foundation::{NSError, NSObjectProtocol, NSString, ns_string}; + use std::time::Duration; + + define_class!( + #[unsafe(super(CBPeripheral))] + #[thread_kind = AnyThread] + #[ivars = Retained] + struct TestPeripheral; + + unsafe impl NSObjectProtocol for TestPeripheral {} + + impl TestPeripheral { + #[unsafe(method_id(identifier))] + fn identifier(&self) -> Retained { + self.ivars().clone() + } + + #[unsafe(method_id(name))] + fn name(&self) -> Option> { + None + } + + #[unsafe(method(maximumWriteValueLengthForType:))] + fn maximum_write_value_length_for_type( + &self, + _write_type: CBCharacteristicWriteType, + ) -> usize { + 0 + } + } + ); + + impl TestPeripheral { + fn new(identifier: Retained) -> Retained { + let this = Self::alloc().set_ivars(identifier); + unsafe { msg_send![super(this), init] } + } + } + + #[test] + fn maximum_write_value_length_is_converted_to_att_mtu() { + assert_eq!(maximum_write_value_length_to_att_mtu(20), Ok(23)); + assert_eq!(maximum_write_value_length_to_att_mtu(512), Ok(515)); + assert_eq!( + maximum_write_value_length_to_att_mtu(u16::MAX as usize - 3), + Ok(u16::MAX) + ); + } + + #[test] + fn zero_maximum_write_value_length_uses_default_mtu() { + assert_eq!( + maximum_write_value_length_to_att_mtu(0), + Ok(crate::api::DEFAULT_MTU_SIZE) + ); + } + + #[test] + fn unrepresentable_maximum_write_value_length_is_rejected() { + assert!(maximum_write_value_length_to_att_mtu(u16::MAX as usize).is_err()); + assert!(maximum_write_value_length_to_att_mtu(usize::MAX).is_err()); + } + + #[tokio::test] + async fn descriptor_discovery_error_completes_service_discovery_without_descriptors() { + let peripheral_uuid = Uuid::from_u128(0x12345678_1234_5678_1234_567812345678); + let peripheral_uuid_string = NSString::from_str(&peripheral_uuid.to_string()); + let peripheral_identifier = + NSUUID::initWithUUIDString(NSUUID::alloc(), &peripheral_uuid_string) + .expect("valid peripheral UUID"); + let peripheral = TestPeripheral::new(peripheral_identifier); + let service_uuid = Uuid::from_u128(0x0000180f_0000_1000_8000_00805f9b34fb); + let characteristic_uuid = Uuid::from_u128(0x00002a19_0000_1000_8000_00805f9b34fb); + let service_cbuuid = uuid_to_cbuuid(service_uuid); + let characteristic_cbuuid = uuid_to_cbuuid(characteristic_uuid); + let characteristic = unsafe { + CBMutableCharacteristic::initWithType_properties_value_permissions( + CBMutableCharacteristic::alloc(), + &characteristic_cbuuid, + CBCharacteristicProperties::Read, + None, + CBAttributePermissions::Readable, + ) + }; + let service = unsafe { + CBMutableService::initWithType_primary(CBMutableService::alloc(), &service_cbuuid, true) + }; + let characteristic: Retained = Retained::into_super(characteristic); + let characteristics = NSArray::from_retained_slice(&[characteristic.clone()]); + unsafe { service.setCharacteristics(Some(&characteristics)) }; + let service: Retained = Retained::into_super(service); + + let (event_sender, _) = mpsc::channel(1); + let mut internal = + PeripheralInternal::new(Retained::into_super(peripheral.clone()), event_sender); + internal.services.insert( + service_uuid, + ServiceInternal { + cbservice: service, + characteristics: HashMap::from([( + characteristic_uuid, + CharacteristicInternal::new(characteristic.clone()), + )]), + discovered: false, + }, + ); + let discovery = CoreBluetoothReplyFuture::default(); + internal.services_discovered_future_state = Some(discovery.get_state_clone()); + + let (delegate_sender, mut delegate_receiver) = mpsc::channel(1); + let delegate = CentralDelegate::new(delegate_sender); + let error = NSError::new(1, ns_string!("BtlePlugCoreBluetoothTests")); + unsafe { + delegate.peripheral_didDiscoverDescriptorsForCharacteristic_error( + &peripheral, + &characteristic, + Some(&error), + ); + } + + let event = tokio::time::timeout(Duration::from_secs(1), delegate_receiver.next()) + .await + .expect("descriptor error callback did not emit an event") + .expect("delegate event channel closed"); + let CentralDelegateEvent::DiscoveredCharacteristicDescriptors { + peripheral_uuid: event_peripheral_uuid, + service_uuid: event_service_uuid, + characteristic_uuid: event_characteristic_uuid, + descriptors, + } = event + else { + panic!("unexpected delegate event: {event:?}"); + }; + assert_eq!(event_peripheral_uuid, peripheral_uuid); + assert_eq!(event_service_uuid, service_uuid); + assert_eq!(event_characteristic_uuid, characteristic_uuid); + assert!(descriptors.is_empty()); + + internal.set_characteristic_descriptors( + event_service_uuid, + event_characteristic_uuid, + descriptors, + ); + let reply = tokio::time::timeout(Duration::from_secs(1), discovery) + .await + .expect("service discovery remained pending after descriptor error"); + let CoreBluetoothReply::ServicesDiscovered(services, mtu) = reply else { + panic!("unexpected discovery reply: {reply:?}"); + }; + assert_eq!(mtu, crate::api::DEFAULT_MTU_SIZE); + let characteristic = services + .iter() + .find(|service| service.uuid == service_uuid) + .and_then(|service| { + service + .characteristics + .iter() + .find(|characteristic| characteristic.uuid == characteristic_uuid) + }) + .expect("discovered characteristic"); + assert!(characteristic.descriptors.is_empty()); + + // CBPeripheral has no public initializer suitable for tests, so this + // subclass must not run CoreBluetooth's private destruction path. + std::mem::forget(internal); + std::mem::forget(peripheral); + } +} + pub fn run_corebluetooth_thread( event_sender: Sender, ) -> Result, Error> { diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 229daaa6..1796055e 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -100,6 +100,7 @@ impl Peripheral { address_type: None, local_name, advertisement_name, + appearance: None, tx_power_level: None, rssi: None, manufacturer_data: HashMap::new(), @@ -193,7 +194,7 @@ impl Peripheral { } } }); - Self { shared: shared } + Self { shared } } pub(super) fn update_name( @@ -202,12 +203,37 @@ impl Peripheral { advertisement_name: Option, ) { if let Ok(mut props) = self.shared.properties.lock() { - props.local_name = local_name; - props.advertisement_name = advertisement_name; + let PeripheralProperties { + local_name: current_local_name, + advertisement_name: current_advertisement_name, + .. + } = &mut *props; + merge_names( + current_local_name, + current_advertisement_name, + local_name, + advertisement_name, + ); } } } +fn merge_names( + local_name: &mut Option, + advertisement_name: &mut Option, + new_local_name: Option, + new_advertisement_name: Option, +) { + if let Some(name) = new_advertisement_name { + *local_name = Some(name.clone()); + *advertisement_name = Some(name); + } else if advertisement_name.is_none() + && let Some(name) = new_local_name + { + *local_name = Some(name); + } +} + impl Display for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { // let connected = if self.is_connected() { " connected" } else { "" }; @@ -218,6 +244,59 @@ impl Display for Peripheral { } } +#[cfg(test)] +mod tests { + use super::merge_names; + + #[test] + fn advertisement_name_takes_precedence_over_gap_name() { + let mut local_name = Some("Longer GAP name".to_string()); + let mut advertisement_name = None; + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("Short GAP".to_string()), + Some("Complete".to_string()), + ); + + assert_eq!(local_name.as_deref(), Some("Complete")); + assert_eq!(advertisement_name.as_deref(), Some("Complete")); + } + + #[test] + fn absent_advertisement_does_not_erase_or_override_it() { + let mut local_name = Some("Complete".to_string()); + let mut advertisement_name = Some("Complete".to_string()); + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("Different GAP name".to_string()), + None, + ); + + assert_eq!(local_name.as_deref(), Some("Complete")); + assert_eq!(advertisement_name.as_deref(), Some("Complete")); + } + + #[test] + fn gap_name_is_used_until_an_advertisement_name_arrives() { + let mut local_name = None; + let mut advertisement_name = None; + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("GAP name".to_string()), + None, + ); + + assert_eq!(local_name.as_deref(), Some("GAP name")); + assert_eq!(advertisement_name, None); + } +} + impl Debug for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Peripheral") @@ -288,6 +367,9 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::Connected => { + self.shared + .mtu + .store(api::DEFAULT_MTU_SIZE, std::sync::atomic::Ordering::Relaxed); self.shared .emit_event(CentralEvent::DeviceConnected(self.shared.uuid.into())); } @@ -330,8 +412,11 @@ impl api::Peripheral for Peripheral { }) .await?; match fut.await { - CoreBluetoothReply::ServicesDiscovered(services) => { + CoreBluetoothReply::ServicesDiscovered(services, mtu) => { *(self.shared.services.lock().map_err(Into::::into)?) = services; + self.shared + .mtu + .store(mtu, std::sync::atomic::Ordering::Relaxed); return Ok(()); } CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), @@ -458,7 +543,13 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::Ok => {} - reply => panic!("Unexpected reply: {:?}", reply), + CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), + reply => { + return Err(Error::RuntimeError(format!( + "Unexpected reply: {:?}", + reply + ))); + } } Ok(()) } @@ -495,9 +586,10 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::ReadResult(chars) => Ok(chars), - _ => { - panic!("Shouldn't get anything but read result!"); - } + CoreBluetoothReply::Err(msg) => Err(Error::RuntimeError(msg)), + _ => Err(Error::RuntimeError( + "Unexpected reply for descriptor read".into(), + )), } } } diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 5e0a4f2b..36d49e61 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -1,9 +1,9 @@ -use super::jni_utils::exceptions::try_block; use super::{ jni::{ - global_jvm, + jvm, objects::{JScanFilter, JScanResult}, }, + jni_utils::exceptions::throwable_to_string, peripheral::{Peripheral, PeripheralId}, }; use crate::{ @@ -13,11 +13,9 @@ use crate::{ }; use async_trait::async_trait; use futures::stream::Stream; -use jni::objects::JClass; use jni::{ - JNIEnv, - objects::{GlobalRef, JObject, JString}, - strings::JavaStr, + Env, jni_sig, jni_str, + objects::{Global, JObject, JString}, sys::jboolean, }; use std::{ @@ -30,7 +28,7 @@ use std::{ #[derive(Clone)] pub struct Adapter { manager: Arc>, - internal: GlobalRef, + internal: Arc>>, } impl Debug for Adapter { @@ -43,30 +41,31 @@ impl Debug for Adapter { impl Adapter { pub(crate) fn new() -> Result { - let env = global_jvm().get_env()?; - - let obj = env.new_object( - "com/nonpolynomial/btleplug/android/impl/Adapter", - "()V", - &[], - )?; - let internal = env.new_global_ref(obj)?; - let adapter = Self { - manager: Arc::new(AdapterManager::default()), - internal, - }; - env.set_rust_field(obj, "handle", adapter.clone())?; - - Ok(adapter) + jvm()?.attach_current_thread(|env| { + let obj = env.new_object( + jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"), + jni_sig!("()V"), + &[], + )?; + let internal = Arc::new(env.new_global_ref(&obj)?); + let adapter = Self { + manager: Arc::new(AdapterManager::default()), + internal, + }; + unsafe { env.set_rust_field(&obj, jni_str!("handle"), adapter.clone()) }?; + + Ok(adapter) + }) } - pub fn report_scan_result(&self, scan_result: JObject) -> Result { - use std::convert::TryInto; - - let env = global_jvm().get_env()?; - let scan_result = JScanResult::from_env(&env, scan_result)?; - - let (addr, properties): (BDAddr, Option) = scan_result.try_into()?; + pub fn report_scan_result<'a>( + &self, + env: &mut Env<'a>, + scan_result: JObject<'a>, + ) -> Result { + let scan_result = env.cast_local::(scan_result)?; + let (addr, properties): (BDAddr, Option) = + scan_result.to_peripheral_properties(env)?; match self.manager.peripheral(&PeripheralId(addr)) { Some(p) => match properties { @@ -74,10 +73,7 @@ impl Adapter { self.report_properties(&p, properties, false); Ok(p) } - None => { - //self.manager.emit(CentralEvent::DeviceDisconnected(addr)); - Err(Error::DeviceNotFound) - } + None => Err(Error::DeviceNotFound), }, None => match properties { Some(properties) => { @@ -91,10 +87,12 @@ impl Adapter { } fn add(&self, address: BDAddr) -> Result { - let env = global_jvm().get_env()?; - let peripheral = Peripheral::new(&env, self.internal.as_obj(), address)?; - self.manager.add_peripheral(peripheral.clone()); - Ok(peripheral) + jvm()?.attach_current_thread(|env| { + let local_adapter = env.new_local_ref(self.internal.as_obj())?; + let peripheral = Peripheral::new(env, local_adapter, address)?; + self.manager.add_peripheral(peripheral.clone()); + Ok(peripheral) + }) } fn report_properties( @@ -130,7 +128,6 @@ impl Central for Adapter { type Peripheral = Peripheral; async fn adapter_info(&self) -> Result { - // TODO: Get information about the adapter. Ok("Android".to_string()) } @@ -139,41 +136,54 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - let env = global_jvm().get_env()?; - let filter = JScanFilter::new(&env, filter)?; - try_block(&env, || { - env.call_method( - &self.internal, - "startScan", - "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", - &[filter.into()], - )?; - Ok(Ok(())) - }) - .catch( - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - ) - .unwrap() - .as_obj(), - ), - |_ex| Ok(Err(Error::NoAdapterAvailable)), - ) - .catch("java/lang/RuntimeException", |ex| { - let msg = env - .call_method(ex, "getMessage", "()Ljava/lang/String;", &[])? - .l()?; - let msgstr: String = env.get_string(msg.into())?.into(); - Ok(Err(Error::RuntimeError(msgstr))) + jvm()?.attach_current_thread(|env| { + let filter = JScanFilter::new(env, filter)?; + let filter_obj: JObject = filter.into(); + match env.call_method( + self.internal.as_obj(), + jni_str!("startScan"), + jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V"), + &[(&filter_obj).into()], + ) { + Ok(_) => Ok(()), + Err(jni::errors::Error::JavaException) => { + let ex = env.exception_occurred().unwrap(); + env.exception_clear(); + + let no_adapter_class = ::lookup_class( + env, + &Default::default(), + )?; + + if env.is_instance_of(&ex, &*no_adapter_class)? { + Err(Error::NoAdapterAvailable) + } else if env.is_instance_of(&ex, jni_str!("java/lang/RuntimeException"))? { + let msg = env + .call_method(&ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? + .l()?; + let jstr = env.cast_local::(msg)?; + let msgstr = String::from(jstr.mutf8_chars(env)?); + Err(Error::RuntimeError(msgstr)) + } else { + let desc = throwable_to_string(env, &ex)?; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) + } + } + Err(e) => Err(e.into()), + } }) - .result()? } async fn stop_scan(&self) -> Result<()> { - let env = global_jvm().get_env()?; - env.call_method(&self.internal, "stopScan", "()V", &[])?; - Ok(()) + jvm()?.attach_current_thread(|env| { + env.call_method( + self.internal.as_obj(), + jni_str!("stopScan"), + jni_sig!("()V"), + &[], + )?; + Ok(()) + }) } async fn peripherals(&self) -> Result> { @@ -195,32 +205,38 @@ impl Central for Adapter { Ok(()) } + async fn adapter_address(&self) -> Result> { + // Ordinary Android applications cannot access the local factory address. + Ok(None) + } + async fn adapter_state(&self) -> Result { Ok(CentralState::Unknown) } } -pub(crate) fn adapter_report_scan_result_internal( - env: &JNIEnv, - obj: JObject, - scan_result: JObject, +pub(crate) fn adapter_report_scan_result_internal<'a>( + env: &mut Env<'a>, + obj: &JObject, + scan_result: JObject<'a>, ) -> crate::Result<()> { - let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; - adapter.report_scan_result(scan_result)?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, jni_str!("handle")) }?; + let adapter_clone = adapter.clone(); + drop(adapter); + adapter_clone.report_scan_result(env, scan_result)?; Ok(()) } pub(crate) fn adapter_on_connection_state_changed_internal( - env: &JNIEnv, - obj: JObject, + env: &mut Env, + obj: &JObject, addr: JString, connected: jboolean, ) -> crate::Result<()> { - let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; - let addr_str = JavaStr::from_env(env, addr)?; - let addr_str = addr_str.to_str().map_err(|e| Error::Other(e.into()))?; - let addr = BDAddr::from_str(addr_str)?; - adapter.manager.emit(if connected != 0 { + let addr_str = String::from(addr.mutf8_chars(env)?); + let addr = BDAddr::from_str(&addr_str)?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, jni_str!("handle")) }?; + adapter.manager.emit(if connected { CentralEvent::DeviceConnected(PeripheralId(addr)) } else { CentralEvent::DeviceDisconnected(PeripheralId(addr)) diff --git a/src/droidplug/java/build.gradle b/src/droidplug/java/build.gradle index d005720a..7793f39f 100644 --- a/src/droidplug/java/build.gradle +++ b/src/droidplug/java/build.gradle @@ -12,7 +12,7 @@ android { compileSdk 34 defaultConfig { - minSdk 23 + minSdk 24 versionCode 1 versionName '0.7.3' } diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index cafe87ad..15426e1c 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,129 +1,107 @@ pub mod objects; -use ::jni::{JNIEnv, JavaVM, NativeMethod, objects::JObject}; +use ::jni::errors::ThrowRuntimeExAndDefault; +use ::jni::{ + Env, EnvUnowned, NativeMethod, jni_str, native_method, + objects::{JObject, Reference}, +}; use jni::{objects::JString, sys::jboolean}; -use once_cell::sync::OnceCell; use std::ffi::c_void; +use std::sync::OnceLock; -static GLOBAL_JVM: OnceCell = OnceCell::new(); +static INIT: OnceLock<()> = OnceLock::new(); -pub fn init(env: &JNIEnv) -> crate::Result<()> { - if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { - env.register_native_methods( - "com/nonpolynomial/btleplug/android/impl/Adapter", - &[ - NativeMethod { - name: "reportScanResult".into(), - sig: "(Landroid/bluetooth/le/ScanResult;)V".into(), - fn_ptr: adapter_report_scan_result as *mut c_void, - }, - NativeMethod { - name: "onConnectionStateChanged".into(), - sig: "(Ljava/lang/String;Z)V".into(), - fn_ptr: adapter_on_connection_state_changed as *mut c_void, - }, - ], - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/Peripheral", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/ScanFilter", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NotConnectedException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - )?; +pub fn init(env: &mut Env) -> crate::Result<()> { + match INIT.get() { + Some(()) => Ok(()), + None => { + let result = init_inner(env); + if result.is_ok() { + let _ = INIT.set(()); + } + result + } + } +} + +fn init_inner(env: &mut Env) -> crate::Result<()> { + // Seed the JavaVM singleton so JavaVM::singleton() works from any thread. + env.get_java_vm()?; + { + let adapter_class = + env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?; + unsafe { + env.register_native_methods( + &adapter_class, + &[ + // Can't use native_method! here — JObject maps to Ljava/lang/Object; but the + // Java side declares the parameter as ScanResult. JNI requires exact signature match. + NativeMethod::from_raw_parts( + jni_str!("reportScanResult"), + jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), + adapter_report_scan_result as *mut c_void, + ), + native_method! { + name = "onConnectionStateChanged", + sig = (addr: JString, connected: jboolean) -> (), + fn = adapter_on_connection_state_changed, + }, + ], + )? + }; + use super::jni_utils::{ + future::{JFuture, JFutureException}, + ops::{JFnAdapter, JFnBiFunctionImpl, JFnFunctionImpl, JFnRunnableImpl}, + stream::{JStream, JStreamPoll}, + task::{JPollResult, JWaker}, + }; + use objects::*; - // jni-utils class caching - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/future/Future", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/future/FutureException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnAdapter", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/stream/Stream", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/stream/StreamPoll", - )?; - super::jni_utils::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/Waker")?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/task/PollResult", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnRunnableImpl", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnFunctionImpl", - )?; + let loader = jni::objects::LoaderContext::default(); + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; // FnAdapter native method registration - let fn_adapter_class = - env.auto_local(env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?); - env.register_native_methods( - &fn_adapter_class, + let fn_adapter_class = ::lookup_class(env, &loader)?; + unsafe { + env.register_native_methods( + &*fn_adapter_class, &[ - NativeMethod { - name: "callInternal".into(), - sig: - "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" - .into(), - fn_ptr: super::jni_utils::ops::fn_adapter_call_internal as *mut c_void, - }, - NativeMethod { - name: "closeInternal".into(), - sig: "()V".into(), - fn_ptr: super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, - }, + NativeMethod::from_raw_parts( + jni_str!("callInternal"), + jni_str!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), + super::jni_utils::ops::fn_adapter_call_internal as *mut c_void, + ), + NativeMethod::from_raw_parts( + jni_str!("closeInternal"), + jni_str!("()V"), + super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, + ), ], - )?; + )? + }; } Ok(()) } -pub fn global_jvm() -> &'static JavaVM { - GLOBAL_JVM.get().expect( - "Droidplug has not been initialized. Please initialize it with btleplug::platform::init().", - ) +pub fn jvm() -> crate::Result { + jni::JavaVM::singleton().map_err(|e| crate::Error::Other(Box::new(e))) } impl From<::jni::errors::Error> for crate::Error { @@ -132,16 +110,26 @@ impl From<::jni::errors::Error> for crate::Error { } } -extern "C" fn adapter_report_scan_result(env: JNIEnv, obj: JObject, scan_result: JObject) { - let _ = super::adapter::adapter_report_scan_result_internal(&env, obj, scan_result); +extern "C" fn adapter_report_scan_result<'local>( + mut env: EnvUnowned<'local>, + obj: JObject<'local>, + scan_result: JObject<'local>, +) { + env.with_env(|env| super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result)) + .resolve::(); } -extern "C" fn adapter_on_connection_state_changed( - env: JNIEnv, - obj: JObject, - addr: JString, +fn adapter_on_connection_state_changed<'local>( + env: &mut Env<'local>, + obj: JObject<'local>, + addr: JString<'local>, connected: jboolean, -) { - let _ = - super::adapter::adapter_on_connection_state_changed_internal(&env, obj, addr, connected); +) -> jni::errors::Result<()> { + if let Err(e) = + super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected) + && !env.exception_check() + { + let _ = env.throw(format!("Rust error: {e}")); + } + Ok(()) } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 6e5e051a..ad48b2e4 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -1,347 +1,259 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid}; use jni::{ - JNIEnv, + Env, bind_java_type, errors::Result, - objects::{JClass, JList, JMap, JMethodID, JObject, JString}, - signature::{JavaType, Primitive}, - strings::JavaStr, + jni_sig, jni_str, + objects::{JObject, JString, Reference}, sys::jint, }; -use std::{collections::HashMap, convert::TryFrom, iter::Iterator}; +use std::{collections::HashMap, iter::Iterator}; use uuid::Uuid; use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; -pub struct JPeripheral<'a: 'b, 'b> { - internal: JObject<'a>, - connect: JMethodID<'a>, - disconnect: JMethodID<'a>, - is_connected: JMethodID<'a>, - discover_services: JMethodID<'a>, - read: JMethodID<'a>, - write: JMethodID<'a>, - set_characteristic_notification: JMethodID<'a>, - get_notifications: JMethodID<'a>, - read_descriptor: JMethodID<'a>, - write_descriptor: JMethodID<'a>, - get_device_name: JMethodID<'a>, - request_mtu: JMethodID<'a>, - get_connection_parameters: JMethodID<'a>, - request_connection_priority: JMethodID<'a>, - read_remote_rssi: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JNotConnectedException => "com.nonpolynomial.btleplug.android.impl.NotConnectedException", } -impl<'a: 'b, 'b> ::std::ops::Deref for JPeripheral<'a, 'b> { - type Target = JObject<'a>; +bind_java_type! { + pub JPermissionDeniedException => "com.nonpolynomial.btleplug.android.impl.PermissionDeniedException", +} - fn deref(&self) -> &Self::Target { - &self.internal - } +bind_java_type! { + pub JUnexpectedCallbackException => "com.nonpolynomial.btleplug.android.impl.UnexpectedCallbackException", } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JPeripheral<'a, 'b>) -> JObject<'a> { - other.internal - } +bind_java_type! { + pub JUnexpectedCharacteristicException => "com.nonpolynomial.btleplug.android.impl.UnexpectedCharacteristicException", } -impl<'a: 'b, 'b> JPeripheral<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - //Self::from_env_impl(env, obj) - //let class = env.find_class("com/nonpolynomial/btleplug/android/impl/Peripheral")?; - //Self::from_env_impl(env, obj, class) - Self::from_env_impl(env, obj) - } +bind_java_type! { + pub JNoSuchCharacteristicException => "com.nonpolynomial.btleplug.android.impl.NoSuchCharacteristicException", +} - fn from_env_impl(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - //let class = env.auto_local(class); - let class_static = crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/Peripheral", - ) - .unwrap(); - let class = JClass::from(class_static.as_obj()); +bind_java_type! { + pub JNoBluetoothAdapterException => "com.nonpolynomial.btleplug.android.impl.NoBluetoothAdapterException", +} - let connect = env.get_method_id( - class, - "connect", - "()Lio/github/gedgygedgy/rust/future/Future;", - )?; - let disconnect = env.get_method_id( - class, - "disconnect", - "()Lio/github/gedgygedgy/rust/future/Future;", - )?; - let is_connected = env.get_method_id(class, "isConnected", "()Z")?; - let discover_services = env.get_method_id( - class, - "discoverServices", - "()Lio/github/gedgygedgy/rust/future/Future;", - )?; - let read = env.get_method_id( - class, - "read", - "(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let write = env.get_method_id( - class, - "write", - "(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let set_characteristic_notification = env.get_method_id( - class, - "setCharacteristicNotification", - "(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let get_notifications = env.get_method_id( - class, - "getNotifications", - "()Lio/github/gedgygedgy/rust/stream/Stream;", - )?; - let read_descriptor = env.get_method_id( - class, - "readDescriptor", - "(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let write_descriptor = env.get_method_id( - class, - "writeDescriptor", - "(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let get_device_name = env.get_method_id(class, "getDeviceName", "()Ljava/lang/String;")?; - let request_mtu = env.get_method_id( - class, - "requestMtu", - "(I)Lio/github/gedgygedgy/rust/future/Future;", - )?; - let get_connection_parameters = - env.get_method_id(class, "getConnectionParameters", "()[I")?; - let request_connection_priority = - env.get_method_id(class, "requestConnectionPriority", "(I)Z")?; - let read_remote_rssi = env.get_method_id( - class, - "readRemoteRssi", - "()Lio/github/gedgygedgy/rust/future/Future;", - )?; - Ok(Self { - internal: obj, - connect, - disconnect, - is_connected, - discover_services, - read, - write, - set_characteristic_notification, - get_notifications, - read_descriptor, - write_descriptor, - get_device_name, - request_mtu, - get_connection_parameters, - request_connection_priority, - read_remote_rssi, - env, - }) - } +bind_java_type! { + pub JScanFilterClass => "com.nonpolynomial.btleplug.android.impl.ScanFilter", +} - pub fn new(env: &'b JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { - // let class = env.find_class("com/nonpolynomial/btleplug/android/impl/Peripheral")?; +// JPeripheral: bind_java_type! for class definition only. Methods use domain-specific +// Java types (UUID, Future, Stream, byte[]) whose JNI signatures can't be expressed +// through the macro's Rust-to-JNI type mapping (JObject → Ljava/lang/Object; is wrong). +bind_java_type! { + pub JPeripheral => "com.nonpolynomial.btleplug.android.impl.Peripheral", + methods { + fn is_connected() -> jboolean, + fn request_connection_priority(priority: jint) -> jboolean, + }, +} + +impl JPeripheral<'_> { + pub fn create<'local>( + env: &mut Env<'local>, + adapter: JObject<'local>, + addr: BDAddr, + ) -> Result> { let addr_jstr = env.new_string(format!("{:X}", addr))?; + let class = JPeripheral::lookup_class(env, &Default::default())?; let obj = env.new_object( - JClass::from( - crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/Peripheral", - ) - .unwrap() - .as_obj(), - ), - //class.as_obj(), - "(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V", - &[adapter.into(), addr_jstr.into()], + &*class, + jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V"), + &[(&adapter).into(), (&addr_jstr).into()], )?; - //Self::from_env_impl(env, obj, class) - Self::from_env_impl(env, obj) + env.cast_local::(obj) } +} - pub fn connect(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.connect, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), +impl<'local> JPeripheral<'local> { + pub fn connect(&self, env: &mut Env<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("connect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } - pub fn disconnect(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.disconnect, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + pub fn disconnect(&self, env: &mut Env<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("disconnect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[], )? .l()?; - JFuture::from_env(self.env, future_obj) - } - - pub fn is_connected(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.is_connected, - JavaType::Primitive(Primitive::Boolean), - &[], - )? - .z() + env.cast_local::(raw) } - pub fn discover_services(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.discover_services, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + pub fn discover_services(&self, env: &mut Env<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("discoverServices"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } - pub fn read(&self, uuid: JUuid<'a, 'b>) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.read, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + pub fn read(&self, env: &mut Env<'local>, uuid: &JUuid<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("read"), + jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), &[uuid.into()], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } pub fn write( &self, - uuid: JUuid<'a, 'b>, - data: JObject<'a>, + env: &mut Env<'local>, + uuid: &JUuid<'local>, + data: &JObject<'local>, write_type: jint, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.write, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + ) -> Result> { + let raw = env + .call_method( + self, + jni_str!("write"), + jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), &[uuid.into(), data.into(), write_type.into()], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } pub fn set_characteristic_notification( &self, - uuid: JUuid<'a, 'b>, + env: &mut Env<'local>, + uuid: &JUuid<'local>, enable: bool, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.set_characteristic_notification, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + ) -> Result> { + let raw = env + .call_method( + self, + jni_str!("setCharacteristicNotification"), + jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), &[uuid.into(), enable.into()], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } - pub fn get_notifications(&self) -> Result> { - let stream_obj = self - .env - .call_method_unchecked( - self.internal, - self.get_notifications, - JavaType::Object("Lio/github/gedgygedgy/rust/stream/Stream;".to_string()), + pub fn get_notifications(&self, env: &mut Env<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("getNotifications"), + jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), &[], )? .l()?; - JStream::from_env(self.env, stream_obj) + env.cast_local::(raw) } pub fn read_descriptor( &self, - characteristic: JUuid<'a, 'b>, - uuid: JUuid<'a, 'b>, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.read_descriptor, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + env: &mut Env<'local>, + characteristic: &JUuid<'local>, + uuid: &JUuid<'local>, + ) -> Result> { + let raw = env + .call_method( + self, + jni_str!("readDescriptor"), + jni_sig!( + "(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;" + ), &[characteristic.into(), uuid.into()], )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) + } + + pub fn write_descriptor( + &self, + env: &mut Env<'local>, + characteristic: &JUuid<'local>, + uuid: &JUuid<'local>, + data: &JObject<'local>, + ) -> Result> { + let raw = env + .call_method( + self, + jni_str!("writeDescriptor"), + jni_sig!( + "(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;" + ), + &[characteristic.into(), uuid.into(), data.into()], + )? + .l()?; + env.cast_local::(raw) } - pub fn get_device_name(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_device_name, - JavaType::Object("Ljava/lang/String;".to_string()), + pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { + let obj = env + .call_method( + self, + jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), &[], )? .l()?; if obj.is_null() { Ok(None) } else { - let name_str = self.env.get_string(obj.into())?; - Ok(Some(name_str.into())) + let jstr = env.cast_local::(obj)?; + let name_str = jstr.mutf8_chars(env)?; + Ok(Some(String::from(name_str))) } } - pub fn request_mtu(&self, mtu: jint) -> Result> { - self.env - .call_method_unchecked( - self.internal, - self.request_mtu, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + pub fn request_mtu(&self, env: &mut Env<'local>, mtu: jint) -> Result> { + let raw = env + .call_method( + self, + jni_str!("requestMtu"), + jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), &[mtu.into()], )? - .l() + .l()?; + env.cast_local::(raw) } - pub fn get_connection_parameters(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_connection_parameters, - JavaType::Array(JavaType::Primitive(Primitive::Int).into()), + pub fn get_connection_parameters( + &self, + env: &mut Env<'local>, + ) -> Result> { + let obj = env + .call_method( + self, + jni_str!("getConnectionParameters"), + jni_sig!("()[I"), &[], )? .l()?; if obj.is_null() { return Ok(None); } - let arr = obj.into_inner(); - let len = self.env.get_array_length(arr)?; + let arr = unsafe { jni::objects::JIntArray::from_raw(env, obj.into_raw()) }; + let len = arr.len(env)?; if len < 3 { return Ok(None); } let mut buf = [0i32; 3]; - self.env.get_int_array_region(arr, 0, &mut buf)?; - // interval is in 1.25ms units → microseconds: × 1250 - // timeout is in 10ms units → microseconds: × 10000 + arr.get_region(env, 0, &mut buf)?; Ok(Some(crate::api::ConnectionParameters { interval_us: (buf[0] as u32) * 1250, latency: buf[1] as u16, @@ -349,269 +261,164 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { })) } - pub fn read_remote_rssi(&self) -> Result> { - self.env - .call_method_unchecked( - self.internal, - self.read_remote_rssi, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + pub fn read_remote_rssi(&self, env: &mut Env<'local>) -> Result> { + let raw = env + .call_method( + self, + jni_str!("readRemoteRssi"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[], )? - .l() - } - - pub fn request_connection_priority(&self, priority: jint) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.request_connection_priority, - JavaType::Primitive(Primitive::Boolean), - &[priority.into()], - )? - .z() - } - - pub fn write_descriptor( - &self, - characteristic: JUuid<'a, 'b>, - uuid: JUuid<'a, 'b>, - data: JObject<'a>, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, - self.write_descriptor, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[characteristic.into(), uuid.into(), data.into()], - )? .l()?; - JFuture::from_env(self.env, future_obj) + env.cast_local::(raw) } } -pub struct JBluetoothGattService<'a: 'b, 'b> { - internal: JObject<'a>, - get_uuid: JMethodID<'a>, - //is_primary: JMethodID<'a>, - get_characteristics: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothGattService")?); +// Android SDK types: class definition only, methods use manual JNI signatures +// because return types (UUID, List, byte[]) don't map to JObject. - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - //let is_primary = env.get_method_id(&class, "isPrimary", "()Z;")?; - let get_characteristics = - env.get_method_id(&class, "getCharacteristics", "()Ljava/util/List;")?; - Ok(Self { - internal: obj, - get_uuid, - //is_primary, - get_characteristics, - env, - }) - } +bind_java_type! { + pub JBluetoothGattService => android.bluetooth.BluetoothGattService, +} +impl<'local> JBluetoothGattService<'local> { pub fn is_primary(&self) -> Result { - /* - self.env - .call_method_unchecked( - self.internal, - self.is_primary, - JavaType::Primitive(Primitive::Boolean), - &[], - )? - .z() - */ Ok(true) } - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[], )? .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + let uuid_obj = env.cast_local::(obj)?; + uuid_obj.as_uuid(env) } - pub fn get_characteristics(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_characteristics, - JavaType::Object("Ljava/util/List;".to_string()), + pub fn get_characteristics( + &self, + env: &mut Env<'local>, + ) -> Result>> { + let obj = env + .call_method( + self, + jni_str!("getCharacteristics"), + jni_sig!("()Ljava/util/List;"), &[], )? .l()?; - let chr_list = JList::from_env(self.env, obj)?; - let mut chr_vec = vec![]; - for chr in chr_list.iter()? { - chr_vec.push(JBluetoothGattCharacteristic::from_env(self.env, chr)?); + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; + let mut chr_vec = Vec::with_capacity(size as usize); + for i in 0..size { + let chr = env + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[jni::objects::JValue::from(i)], + )? + .l()?; + chr_vec.push(env.cast_local::(chr)?); } Ok(chr_vec) } } -pub struct JBluetoothGattCharacteristic<'a: 'b, 'b> { - internal: JObject<'a>, - get_uuid: JMethodID<'a>, - get_properties: JMethodID<'a>, - get_value: JMethodID<'a>, - get_descriptors: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JBluetoothGattCharacteristic => android.bluetooth.BluetoothGattCharacteristic, + methods { + fn get_properties_raw { name = "getProperties", sig = () -> jint }, + }, } -impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = - env.auto_local(env.find_class("android/bluetooth/BluetoothGattCharacteristic")?); - - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - let get_properties = env.get_method_id(&class, "getProperties", "()I")?; - let get_descriptors = env.get_method_id(&class, "getDescriptors", "()Ljava/util/List;")?; - let get_value = env.get_method_id(&class, "getValue", "()[B")?; - Ok(Self { - internal: obj, - get_uuid, - get_properties, - get_value, - get_descriptors, - env, - }) - } - - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), +impl<'local> JBluetoothGattCharacteristic<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[], )? .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + let uuid_obj = env.cast_local::(obj)?; + uuid_obj.as_uuid(env) } - pub fn get_properties(&self) -> Result { - let flags = self - .env - .call_method_unchecked( - self.internal, - self.get_properties, - JavaType::Primitive(Primitive::Int), - &[], - )? - .i()?; + pub fn get_properties(&self, env: &mut Env<'local>) -> Result { + let flags = self.get_properties_raw(env)?; Ok(CharPropFlags::from_bits_truncate(flags as u8)) } - pub fn get_value(&self) -> Result> { - let value = self - .env - .call_method_unchecked( - self.internal, - self.get_value, - JavaType::Array(JavaType::Primitive(Primitive::Byte).into()), - &[], - )? + pub fn get_value(&self, env: &mut Env<'local>) -> Result> { + let value = env + .call_method(self, jni_str!("getValue"), jni_sig!("()[B"), &[])? .l()?; - crate::droidplug::jni_utils::arrays::byte_array_to_vec(self.env, value.into_inner()) + let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } - pub fn get_descriptors(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_descriptors, - JavaType::Object("Ljava/util/List;".to_string()), + pub fn get_descriptors( + &self, + env: &mut Env<'local>, + ) -> Result>> { + let obj = env + .call_method( + self, + jni_str!("getDescriptors"), + jni_sig!("()Ljava/util/List;"), &[], )? .l()?; - let desc_list = JList::from_env(self.env, obj)?; - let mut desc_vec = vec![]; - for desc in desc_list.iter()? { - desc_vec.push(JBluetoothGattDescriptor::from_env(self.env, desc)?); + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; + let mut desc_vec = Vec::with_capacity(size as usize); + for i in 0..size { + let desc = env + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[jni::objects::JValue::from(i)], + )? + .l()?; + desc_vec.push(env.cast_local::(desc)?); } Ok(desc_vec) } } -pub struct JBluetoothGattDescriptor<'a: 'b, 'b> { - internal: JObject<'a>, - get_uuid: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JBluetoothGattDescriptor => android.bluetooth.BluetoothGattDescriptor, } -impl<'a: 'b, 'b> JBluetoothGattDescriptor<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothGattDescriptor")?); - - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - Ok(Self { - internal: obj, - get_uuid, - env, - }) - } - - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), +impl<'local> JBluetoothGattDescriptor<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[], )? .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + let uuid_obj = env.cast_local::(obj)?; + uuid_obj.as_uuid(env) } } -pub struct JBluetoothDevice<'a: 'b, 'b> { - internal: JObject<'a>, - get_address: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JBluetoothDevice<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothDevice")?); - - let get_address = env.get_method_id(&class, "getAddress", "()Ljava/lang/String;")?; - Ok(Self { - internal: obj, - get_address, - env, - }) - } - - pub fn get_address(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_address, - JavaType::Object("Ljava/lang/String;".to_string()), - &[], - )? - .l()?; - Ok(obj.into()) - } +bind_java_type! { + pub JBluetoothDevice => android.bluetooth.BluetoothDevice, + methods { + fn get_address() -> JString, + }, } pub struct JScanFilter<'a> { @@ -619,27 +426,21 @@ pub struct JScanFilter<'a> { } impl<'a> JScanFilter<'a> { - pub fn new(env: &'a JNIEnv<'a>, filter: ScanFilter) -> Result { - let uuids = env.new_object_array( - filter.services.len() as i32, - env.find_class("java/lang/String")?, - JObject::null(), + pub fn new(env: &mut Env<'a>, filter: ScanFilter) -> Result { + let uuids = jni::objects::JObjectArray::::new( + env, + filter.services.len(), + &JString::default(), )?; for (idx, uuid) in filter.services.into_iter().enumerate() { let uuid_str = env.new_string(uuid.to_string())?; - env.set_object_array_element(uuids, idx as i32, uuid_str)?; + uuids.set_element(env, idx, &uuid_str)?; } + let class = ::lookup_class(env, &Default::default())?; let obj = env.new_object( - JClass::from( - crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/ScanFilter", - ) - .unwrap() - .as_obj(), - ), - //class.as_obj(), - "([Ljava/lang/String;)V", - &[uuids.into()], + &*class, + jni_sig!("([Ljava/lang/String;)V"), + &[(&uuids).into()], )?; Ok(Self { internal: obj }) } @@ -651,191 +452,176 @@ impl<'a> From> for JObject<'a> { } } -pub struct JScanResult<'a: 'b, 'b> { - internal: JObject<'a>, - get_device: JMethodID<'a>, - get_scan_record: JMethodID<'a>, - get_tx_power: JMethodID<'a>, - get_rssi: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JScanResult => android.bluetooth.le.ScanResult, + methods { + fn get_tx_power() -> jint, + fn get_rssi() -> jint, + }, } -impl<'a: 'b, 'b> JScanResult<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/le/ScanResult")?); - - let get_device = - env.get_method_id(&class, "getDevice", "()Landroid/bluetooth/BluetoothDevice;")?; - let get_scan_record = env.get_method_id( - &class, - "getScanRecord", - "()Landroid/bluetooth/le/ScanRecord;", - )?; - let get_tx_power = env.get_method_id(&class, "getTxPower", "()I")?; - let get_rssi = env.get_method_id(&class, "getRssi", "()I")?; - Ok(Self { - internal: obj, - get_device, - get_scan_record, - get_tx_power, - get_rssi, - env, - }) - } - - pub fn get_device(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_device, - JavaType::Object("Landroid/bluetooth/BluetoothDevice;".to_string()), - &[], - )? - .l()?; - JBluetoothDevice::from_env(self.env, obj) - } - - pub fn get_scan_record(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_scan_record, - JavaType::Object("Landroid/bluetooth/le/ScanRecord;".to_string()), +impl<'local> JScanResult<'local> { + pub fn get_device(&self, env: &mut Env<'local>) -> Result> { + let obj = env + .call_method( + self, + jni_str!("getDevice"), + jni_sig!("()Landroid/bluetooth/BluetoothDevice;"), &[], )? .l()?; - JScanRecord::from_env(self.env, obj) - } - - pub fn get_tx_power(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.get_tx_power, - JavaType::Primitive(Primitive::Int), - &[], - )? - .i() + env.cast_local::(obj) } - pub fn get_rssi(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.get_rssi, - JavaType::Primitive(Primitive::Int), - &[], - )? - .i() + pub fn get_scan_record(&self, env: &mut Env<'local>) -> Result> { + env.call_method( + self, + jni_str!("getScanRecord"), + jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), + &[], + )? + .l() } -} - -impl<'a: 'b, 'b> TryFrom> for (BDAddr, Option) { - type Error = crate::Error; - fn try_from(result: JScanResult<'a, 'b>) -> std::result::Result { + pub fn to_peripheral_properties( + &self, + env: &mut Env<'local>, + ) -> std::result::Result<(BDAddr, Option), crate::Error> { use std::str::FromStr; - let device = result.get_device()?; - - let addr_obj = device.get_address()?; - let addr_str = JavaStr::from_env(result.env, addr_obj)?; - let addr = BDAddr::from_str( - addr_str - .to_str() - .map_err(|e| Self::Error::Other(e.into()))?, - )?; + let device = self.get_device(env)?; + let addr_jstr = device.get_address(env)?; + let addr_str = String::from(addr_jstr.mutf8_chars(env)?); + let addr = BDAddr::from_str(&addr_str)?; - let record = result.get_scan_record()?; - let record_obj: &JObject = &record; - let properties = if result - .env - .is_same_object(record_obj.clone(), JObject::null())? - { + let record_obj = self.get_scan_record(env)?; + let properties = if record_obj.is_null() { None } else { - let device_name_obj = record.get_device_name()?; - let device_name = if result - .env - .is_same_object(device_name_obj, JObject::null())? - { + let record = env.cast_local::(record_obj)?; + let device_name_obj = record.get_device_name(env)?; + let device_name = if env.is_same_object(&device_name_obj, JObject::null())? { None } else { - let device_name_str = JavaStr::from_env(result.env, device_name_obj)?; - // On Android, there is a chance that a device name may not actually be valid UTF-8. - // We're given the full buffer, regardless of if it's just UTF-8 characters, - // possibly c str with null characters, or whatever. We should try UTF-8 first, if - // that doesn't work out, see if there's a null termination character in it and try - // parsing that. + let device_name_jstr = env.cast_local::(device_name_obj)?; + let device_name_str = String::from(device_name_jstr.mutf8_chars(env)?); Some( - String::from_utf8_lossy(device_name_str.to_bytes()) + device_name_str .chars() .filter(|&c| c != '\u{fffd}') .collect(), ) }; - let tx_power_level = result.get_tx_power()?; - const TX_POWER_NOT_PRESENT: jint = 127; // from ScanResult documentation + let tx_power_level = self.get_tx_power(env)?; + const TX_POWER_NOT_PRESENT: jint = 127; let tx_power_level = if tx_power_level == TX_POWER_NOT_PRESENT { - None + match record.get_tx_power_level(env)? { + TX_POWER_NOT_PRESENT => None, + tx_power_level => Some(tx_power_level as i16), + } } else { Some(tx_power_level as i16) }; - let rssi = Some(result.get_rssi()? as i16); + let rssi = Some(self.get_rssi(env)? as i16); + let appearance = record + .get_bytes(env)? + .as_deref() + .and_then(crate::advertisement::parse_appearance_from_advertisement); - let manufacturer_specific_data_array = record.get_manufacturer_specific_data()?; - let manufacturer_specific_data_obj: &JObject = &manufacturer_specific_data_array; + let mfr_data_obj = record.get_manufacturer_specific_data(env)?; let mut manufacturer_data = HashMap::new(); - if !result - .env - .is_same_object(manufacturer_specific_data_obj.clone(), JObject::null())? - { - for item in manufacturer_specific_data_array.iter() { - let (index, data) = item?; - - let index = index as u16; - let data = crate::droidplug::jni_utils::arrays::byte_array_to_vec( - result.env, - data.into_inner(), - )?; - manufacturer_data.insert(index, data); + if !mfr_data_obj.is_null() { + let sparse_arr = env.cast_local::(mfr_data_obj)?; + let size = sparse_arr.size(env)?; + for i in 0..size { + let key = sparse_arr.key_at(env, i)?; + let value = sparse_arr.value_at(env, i)?; + let value_arr = + unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; + let data = + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr)?; + manufacturer_data.insert(key as u16, data); } } - let service_data_map = record.get_service_data()?; - let service_data_obj: &JObject = &service_data_map; + let service_data_obj = record.get_service_data(env)?; let mut service_data = HashMap::new(); - if !result - .env - .is_same_object(service_data_obj.clone(), JObject::null())? - { - for (key, value) in service_data_map.iter()? { - let uuid = JParcelUuid::from_env(result.env, key)? - .get_uuid()? - .as_uuid()?; - let data = crate::droidplug::jni_utils::arrays::byte_array_to_vec( - result.env, - value.into_inner(), - )?; + if !env.is_same_object(&service_data_obj, JObject::null())? { + let entry_set = env + .call_method( + &service_data_obj, + jni_str!("entrySet"), + jni_sig!("()Ljava/util/Set;"), + &[], + )? + .l()?; + let iter_obj = env + .call_method( + &entry_set, + jni_str!("iterator"), + jni_sig!("()Ljava/util/Iterator;"), + &[], + )? + .l()?; + while env + .call_method(&iter_obj, jni_str!("hasNext"), jni_sig!("()Z"), &[])? + .z()? + { + let entry = env + .call_method( + &iter_obj, + jni_str!("next"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? + .l()?; + let key = env + .call_method( + &entry, + jni_str!("getKey"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? + .l()?; + let value = env + .call_method( + &entry, + jni_str!("getValue"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? + .l()?; + let parcel_uuid = env.cast_local::(key)?; + let juuid = parcel_uuid.get_uuid(env)?; + let uuid = juuid.as_uuid(env)?; + let value_arr = + unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; + let data = + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr)?; service_data.insert(uuid, data); } } - let services_list = record.get_service_uuids()?; - let services_obj: &JObject = &services_list; + let services_obj = record.get_service_uuids(env)?; let mut services = Vec::new(); - if !result - .env - .is_same_object(services_obj.clone(), JObject::null())? - { - for obj in services_list.iter()? { - let uuid = JParcelUuid::from_env(result.env, obj)? - .get_uuid()? - .as_uuid()?; + if !env.is_same_object(&services_obj, JObject::null())? { + let size = env + .call_method(&services_obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; + for i in 0..size { + let obj = env + .call_method( + &services_obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[jni::objects::JValue::from(i)], + )? + .l()?; + let parcel_uuid = env.cast_local::(obj)?; + let juuid = parcel_uuid.get_uuid(env)?; + let uuid = juuid.as_uuid(env)?; services.push(uuid); } } @@ -845,6 +631,7 @@ impl<'a: 'b, 'b> TryFrom> for (BDAddr, Option TryFrom> for (BDAddr, Option { - internal: JObject<'a>, - get_device_name: JMethodID<'a>, - get_tx_power_level: JMethodID<'a>, - get_manufacturer_specific_data: JMethodID<'a>, - get_service_data: JMethodID<'a>, - get_service_uuids: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(scan_record: JScanRecord<'a, 'b>) -> Self { - scan_record.internal - } -} - -impl<'a: 'b, 'b> ::std::ops::Deref for JScanRecord<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } +bind_java_type! { + pub JScanRecord => android.bluetooth.le.ScanRecord, + methods { + fn get_tx_power_level() -> jint, + }, } -impl<'a: 'b, 'b> JScanRecord<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/le/ScanRecord")?); - - let get_device_name = env.get_method_id(&class, "getDeviceName", "()Ljava/lang/String;")?; - let get_tx_power_level = env.get_method_id(&class, "getTxPowerLevel", "()I")?; - let get_manufacturer_specific_data = env.get_method_id( - &class, - "getManufacturerSpecificData", - "()Landroid/util/SparseArray;", - )?; - let get_service_data = env.get_method_id(&class, "getServiceData", "()Ljava/util/Map;")?; - let get_service_uuids = - env.get_method_id(&class, "getServiceUuids", "()Ljava/util/List;")?; - Ok(Self { - internal: obj, - get_device_name, - get_tx_power_level, - get_manufacturer_specific_data, - get_service_data, - get_service_uuids, - env, - }) - } - - pub fn get_device_name(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_device_name, - JavaType::Object("Ljava/lang/String;".to_string()), - &[], - )? - .l()?; - Ok(obj.into()) - } - - pub fn get_tx_power_level(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.get_tx_power_level, - JavaType::Primitive(Primitive::Int), - &[], - )? - .i() - } - - pub fn get_manufacturer_specific_data(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_manufacturer_specific_data, - JavaType::Object("Landroid/util/SparseArray;".to_string()), - &[], - )? - .l()?; - JSparseArray::from_env(self.env, obj) - } - - pub fn get_service_data(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_service_data, - JavaType::Object("Ljava/util/Map;".to_string()), - &[], - )? - .l()?; - JMap::from_env(self.env, obj) - } - - pub fn get_service_uuids(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_service_uuids, - JavaType::Object("Ljava/util/List;".to_string()), - &[], - )? +impl<'local> JScanRecord<'local> { + pub fn get_bytes(&self, env: &mut Env<'local>) -> Result>> { + let value = env + .call_method(self, jni_str!("getBytes"), jni_sig!("()[B"), &[])? .l()?; - JList::from_env(self.env, obj) - } -} - -#[derive(Clone)] -pub struct JSparseArray<'a: 'b, 'b> { - internal: JObject<'a>, - size: JMethodID<'a>, - key_at: JMethodID<'a>, - value_at: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(sparse_array: JSparseArray<'a, 'b>) -> Self { - sparse_array.internal - } -} - -impl<'a: 'b, 'b> ::std::ops::Deref for JSparseArray<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a: 'b, 'b> JSparseArray<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/util/SparseArray")?); - - let size = env.get_method_id(&class, "size", "()I")?; - let key_at = env.get_method_id(&class, "keyAt", "(I)I")?; - let value_at = env.get_method_id(&class, "valueAt", "(I)Ljava/lang/Object;")?; - Ok(Self { - internal: obj, - size, - key_at, - value_at, - env, - }) + if value.is_null() { + Ok(None) + } else { + let value = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value).map(Some) + } } - pub fn size(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.size, - JavaType::Primitive(Primitive::Int), - &[], - )? - .i() + pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { + env.call_method( + self, + jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l() } - pub fn key_at(&self, index: jint) -> Result { - self.env - .call_method_unchecked( - self.internal, - self.key_at, - JavaType::Primitive(Primitive::Int), - &[index.into()], - )? - .i() + pub fn get_manufacturer_specific_data(&self, env: &mut Env<'local>) -> Result> { + env.call_method( + self, + jni_str!("getManufacturerSpecificData"), + jni_sig!("()Landroid/util/SparseArray;"), + &[], + )? + .l() } - pub fn value_at(&self, index: jint) -> Result> { - self.env - .call_method_unchecked( - self.internal, - self.value_at, - JavaType::Object("Ljava/lang/Object;".to_string()), - &[index.into()], - )? - .l() + pub fn get_service_data(&self, env: &mut Env<'local>) -> Result> { + env.call_method( + self, + jni_str!("getServiceData"), + jni_sig!("()Ljava/util/Map;"), + &[], + )? + .l() } - pub fn iter(&self) -> JSparseArrayIter<'a, 'b> { - JSparseArrayIter { - internal: self.clone(), - index: 0, - } + pub fn get_service_uuids(&self, env: &mut Env<'local>) -> Result> { + env.call_method( + self, + jni_str!("getServiceUuids"), + jni_sig!("()Ljava/util/List;"), + &[], + )? + .l() } } -pub struct JSparseArrayIter<'a: 'b, 'b> { - internal: JSparseArray<'a, 'b>, - index: jint, -} - -impl<'a: 'b, 'b> JSparseArrayIter<'a, 'b> { - fn next_internal(&mut self) -> Result)>> { - let size = self.internal.size()?; - Ok(if self.index >= size { - None - } else { - let key = self.internal.key_at(self.index)?; - let value = self.internal.value_at(self.index)?; - self.index += 1; - Some((key, value)) - }) - } +bind_java_type! { + pub JSparseArray => android.util.SparseArray, + methods { + fn size() -> jint, + fn key_at(index: jint) -> jint, + fn value_at(index: jint) -> JObject, + }, } -impl<'a: 'b, 'b> Iterator for JSparseArrayIter<'a, 'b> { - type Item = Result<(jint, JObject<'a>)>; - - fn next(&mut self) -> Option { - self.next_internal().transpose() - } +bind_java_type! { + pub JParcelUuid => android.os.ParcelUuid, } -pub struct JParcelUuid<'a: 'b, 'b> { - internal: JObject<'a>, - get_uuid: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JParcelUuid<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/os/ParcelUuid")?); - - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - Ok(Self { - internal: obj, - get_uuid, - env, - }) - } - pub fn get_uuid(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), +impl<'local> JParcelUuid<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result> { + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[], )? .l()?; - JUuid::from_env(self.env, obj) + env.cast_local::(obj) } } diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index e471fb9d..48682c37 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -1,25 +1,22 @@ -use jni::{ - JNIEnv, - errors::Result, - sys::{jbyte, jbyteArray, jint}, -}; +use jni::{Env, errors::Result, objects::JByteArray, sys::jbyte}; use std::slice; -/// Create a new Java byte array from the given slice. -pub fn slice_to_byte_array<'a, 'b>(env: &'a JNIEnv<'a>, slice: &'b [u8]) -> Result { - let obj = env.new_byte_array(slice.len() as jint)?; +pub fn slice_to_byte_array<'local>( + env: &mut Env<'local>, + slice: &[u8], +) -> Result> { + let obj = env.new_byte_array(slice.len())?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; - env.set_byte_array_region(obj, 0, slice)?; + obj.set_region(env, 0, slice)?; Ok(obj) } -/// Get a [`Vec`] of bytes from the given Java byte array. -pub fn byte_array_to_vec<'a>(env: &'a JNIEnv<'a>, obj: jbyteArray) -> Result> { - let size = env.get_array_length(obj)? as usize; +pub fn byte_array_to_vec(env: &Env, array: &JByteArray) -> Result> { + let size = array.len(env)?; let mut result = Vec::with_capacity(size); unsafe { let result_slice = slice::from_raw_parts_mut(result.as_mut_ptr() as *mut jbyte, size); - env.get_byte_array_region(obj, 0, result_slice)?; + array.get_region(env, 0, result_slice)?; result.set_len(size); } Ok(result) @@ -31,24 +28,28 @@ mod test { #[test] fn test_slice_to_byte_array() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let obj = super::slice_to_byte_array(env, &[1, 2, 3, 4, 5]).unwrap(); - assert_eq!(env.get_array_length(obj).unwrap(), 5); + assert_eq!(obj.len(env).unwrap(), 5); let mut bytes = [0i8; 5]; - env.get_byte_array_region(obj, 0, &mut bytes).unwrap(); + obj.get_region(env, 0, &mut bytes).unwrap(); assert_eq!(bytes, [1, 2, 3, 4, 5]); - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_byte_array_to_vec() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let obj = env.new_byte_array(5).unwrap(); - env.set_byte_array_region(obj, 0, &[1, 2, 3, 4, 5]).unwrap(); + obj.set_region(env, 0, &[1, 2, 3, 4, 5]).unwrap(); - let vec = super::byte_array_to_vec(env, obj).unwrap(); + let vec = super::byte_array_to_vec(env, &obj).unwrap(); assert_eq!(vec, vec![1, 2, 3, 4, 5]); - }); + Ok(()) + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs deleted file mode 100644 index 3b0f63ff..00000000 --- a/src/droidplug/jni_utils/classcache.rs +++ /dev/null @@ -1,22 +0,0 @@ -use dashmap::DashMap; -use jni::{JNIEnv, errors::Result, objects::GlobalRef}; -use once_cell::sync::OnceCell; - -static CLASSCACHE: OnceCell> = OnceCell::new(); - -pub fn find_add_class(env: &JNIEnv, classname: &str) -> Result<()> { - let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - cache.insert( - classname.to_owned(), - env.new_global_ref(env.find_class(classname).unwrap()) - .unwrap(), - ); - Ok(()) -} - -pub fn get_class(classname: &str) -> Option { - let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - cache - .get(classname) - .and_then(|pair| Some(pair.value().clone())) -} diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 6f014d52..86f269f7 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -1,20 +1,37 @@ +#![allow(dead_code)] + use jni::{ - JNIEnv, + Env, descriptors::Desc, errors::Error, - objects::{JClass, JObject, JThrowable}, + jni_sig, jni_str, + objects::{JClass, JObject, JString, JThrowable}, }; use std::{ any::Any, - convert::TryFrom, panic::{UnwindSafe, catch_unwind, resume_unwind}, sync::MutexGuard, }; +pub(crate) fn throwable_to_string( + env: &mut Env, + throwable: &JThrowable, +) -> jni::errors::Result { + let msg = env + .call_method( + throwable, + jni_str!("toString"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + let jstr = env.cast_local::(msg)?; + Ok(String::from(jstr.mutf8_chars(env)?)) +} + /// Result from [`try_block`]. This object can be chained into /// [`catch`](TryCatchResult::catch) calls to catch exceptions. -pub struct TryCatchResult<'a: 'b, 'b, T> { - env: &'b JNIEnv<'a>, +pub struct TryCatchResult { try_result: Result, Error>, catch_result: Option>, } @@ -22,68 +39,65 @@ pub struct TryCatchResult<'a: 'b, 'b, T> { /// Attempt to execute a block of JNI code. If the code causes an exception /// to be thrown, it will be stored in the resulting [`TryCatchResult`] for /// matching with [`catch`](TryCatchResult::catch). -pub fn try_block<'a: 'b, 'b, T>( - env: &'b JNIEnv<'a>, - block: impl FnOnce() -> Result, -) -> TryCatchResult<'a, 'b, T> { +pub fn try_block( + env: &mut Env, + block: impl FnOnce(&mut Env) -> Result, +) -> TryCatchResult { TryCatchResult { - env, - try_result: (|| { - if env.exception_check()? { - Err(Error::JavaException) - } else { - Ok(block()) - } - })(), + try_result: if env.exception_check() { + Err(Error::JavaException) + } else { + Ok(block(env)) + }, catch_result: None, } } -impl<'a: 'b, 'b, T> TryCatchResult<'a, 'b, T> { - pub fn catch( +impl TryCatchResult { + pub fn catch<'local>( self, - class: impl Desc<'a, JClass<'a>>, - block: impl FnOnce(JThrowable<'a>) -> Result, + env: &mut Env<'local>, + class: impl Desc<'local, JClass<'local>>, + block: impl FnOnce(&mut Env<'local>, JThrowable<'local>) -> Result, ) -> Self { match (self.try_result, self.catch_result) { (Err(e), _) => Self { - env: self.env, try_result: Err(e), catch_result: None, }, (Ok(Ok(r)), _) => Self { - env: self.env, try_result: Ok(Ok(r)), catch_result: None, }, (Ok(Err(e)), Some(r)) => Self { - env: self.env, try_result: Ok(Err(e)), catch_result: Some(r), }, (Ok(Err(Error::JavaException)), None) => { - let env = self.env; let catch_result = (|| { - if env.exception_check()? { - let ex = env.exception_occurred()?; - let _auto_local = env.auto_local(ex.clone()); - env.exception_clear()?; - if env.is_instance_of(ex, class)? { - return block(ex).map(|o| Some(o)); + if env.exception_check() + && let Some(ex) = env.exception_occurred() + { + env.exception_clear(); + if env.is_instance_of(&ex, class)? { + return block(env, ex).map(|o| Some(o)); + } + // Rethrow — throw() returns Err(JavaException) on success + match env.throw(&ex) { + Err(Error::JavaException) => {} + Err(e) => return Err(e), + Ok(()) => {} } - env.throw(ex)?; } Ok(None) })() .transpose(); Self { - env, try_result: Ok(Err(Error::JavaException)), catch_result, } } (Ok(Err(e)), None) => Self { - env: self.env, try_result: Ok(Err(e)), catch_result: None, }, @@ -102,62 +116,59 @@ impl<'a: 'b, 'b, T> TryCatchResult<'a, 'b, T> { /// Wrapper for [`JObject`]s that implement /// `io.github.gedgygedgy.rust.panic.PanicException`. -pub struct JPanicException<'a: 'b, 'b> { +pub struct JPanicException<'a> { internal: JThrowable<'a>, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JPanicException<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JThrowable<'a>) -> Result { - Ok(Self { internal: obj, env }) +impl<'a> JPanicException<'a> { + pub fn from_env(obj: JThrowable<'a>) -> Self { + Self { internal: obj } } - pub fn new(env: &'b JNIEnv<'a>, any: Box) -> Result { + pub fn new(env: &mut Env<'a>, any: Box) -> Result { let msg = if let Some(s) = any.downcast_ref::<&str>() { - env.new_string(s)? + env.new_string(s)?.into() } else if let Some(s) = any.downcast_ref::() { - env.new_string(s)? + env.new_string(s)?.into() } else { - JObject::null().into() + JObject::null() }; let obj = env.new_object( - "io/github/gedgygedgy/rust/panic/PanicException", - "(Ljava/lang/String;)V", - &[msg.into()], + jni_str!("io/github/gedgygedgy/rust/panic/PanicException"), + jni_sig!("(Ljava/lang/String;)V"), + &[(&msg).into()], )?; - env.set_rust_field(obj, "any", any)?; - Self::from_env(env, obj.into()) - } - - pub fn get(&self) -> Result>, Error> { - self.env.get_rust_field(self.internal, "any") + unsafe { env.set_rust_field(&obj, jni_str!("any"), any) }?; + let throwable = env.cast_local::(obj)?; + Ok(Self { + internal: throwable, + }) } - pub fn take(&self) -> Result, Error> { - self.env.take_rust_field(self.internal, "any") + pub fn get<'b>( + &self, + env: &'b mut Env, + ) -> Result>, Error> { + unsafe { env.get_rust_field(&self.internal, jni_str!("any")) } } - pub fn resume_unwind(&self) -> Result<(), Error> { - resume_unwind(self.take()?); + pub fn take(&self, env: &mut Env) -> Result, Error> { + unsafe { env.take_rust_field(&self.internal, jni_str!("any")) } } -} - -impl<'a: 'b, 'b> TryFrom> for Box { - type Error = Error; - fn try_from(ex: JPanicException<'a, 'b>) -> Result { - ex.take() + pub fn resume_unwind(&self, env: &mut Env) -> Result<(), Error> { + resume_unwind(self.take(env)?); } } -impl<'a: 'b, 'b> From> for JThrowable<'a> { - fn from(ex: JPanicException<'a, 'b>) -> Self { +impl<'a> From> for JThrowable<'a> { + fn from(ex: JPanicException<'a>) -> Self { ex.internal } } -impl<'a: 'b, 'b> ::std::ops::Deref for JPanicException<'a, 'b> { +impl<'a> ::std::ops::Deref for JPanicException<'a> { type Target = JThrowable<'a>; fn deref(&self) -> &Self::Target { @@ -165,112 +176,141 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JPanicException<'a, 'b> { } } +/// Wraps a caught panic payload in a +/// `io.github.gedgygedgy.rust.panic.PanicException` and throws it. If a Java +/// exception is already pending, it will be added as a suppressed exception. +pub fn throw_panic(env: &mut Env, panic: Box) -> Result<(), Error> { + let old_ex = if env.exception_check() { + let ex = env.exception_occurred(); + env.exception_clear(); + ex + } else { + None + }; + let ex = JPanicException::new(env, panic)?; + + if let Some(old_ex) = old_ex { + env.call_method( + &*ex, + jni_str!("addSuppressed"), + jni_sig!("(Ljava/lang/Throwable;)V"), + &[(&old_ex).into()], + )?; + } + let ex: JThrowable = ex.into(); + // throw() returns Err(JavaException) on success in jni 0.22 + match env.throw(&ex) { + Err(Error::JavaException) => Ok(()), + Err(e) => Err(e), + Ok(()) => Ok(()), + } +} + /// Calls the given closure. If it panics, catch the unwind, wrap it in a /// `io.github.gedgygedgy.rust.panic.PanicException`, and throw it. -pub fn throw_unwind<'a: 'b, 'b, R>( - env: &'b JNIEnv<'a>, +pub fn throw_unwind( + env: &mut Env, f: impl FnOnce() -> R + UnwindSafe, ) -> Result> { - catch_unwind(f).map_err(|e| { - let old_ex = if env.exception_check()? { - let ex = env.exception_occurred()?; - env.exception_clear()?; - Some(ex) - } else { - None - }; - let ex = JPanicException::new(env, e)?; - - if let Some(old_ex) = old_ex { - env.call_method( - ex.clone(), - "addSuppressed", - "(Ljava/lang/Throwable;)V", - &[old_ex.into()], - )?; - } - let ex: JThrowable = ex.into(); - env.throw(ex)?; - Ok(()) - }) + catch_unwind(f).map_err(|e| throw_panic(env, e)) } #[cfg(test)] mod test { - use jni::{JNIEnv, errors::Error, objects::JThrowable}; + use jni::{ + Env, + errors::Error, + jni_sig, jni_str, + objects::{JObject, JThrowable}, + strings::JNIString, + }; use super::super::test_utils; use super::try_block; - fn test_catch<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + fn test_catch( + env: &mut Env, throw_class: Option<&str>, try_result: Result, rethrow: bool, ) -> Result { - let old_ex = if env.exception_check().unwrap() { - let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); - Some(ex) + let old_ex = if env.exception_check() { + let ex = env.exception_occurred(); + env.exception_clear(); + ex } else { None }; let illegal_argument_exception = env - .find_class("java/lang/IllegalArgumentException") + .find_class(jni_str!("java/lang/IllegalArgumentException")) .unwrap(); - if let Some(ex) = old_ex { - env.throw(ex).unwrap(); + if let Some(ref ex) = old_ex { + let _ = env.throw(ex); } let ex = throw_class.map(|c| { - let ex: JThrowable = env.new_object(c, "()V", &[]).unwrap().into(); - ex + let obj = env + .new_object(JNIString::from(c), jni_sig!("()V"), &[]) + .unwrap(); + env.cast_local::(obj).unwrap() }); - try_block(env, || { - if let Some(t) = ex { - env.throw(t).unwrap(); + try_block(env, |env| { + if let Some(ref t) = ex { + let _ = env.throw(t); } try_result }) - .catch(illegal_argument_exception, |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); + .catch(env, illegal_argument_exception, |env, caught| { + assert!(!env.exception_check()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); Ok(1) }) - .catch("java/lang/ArrayIndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - if rethrow { - Err(Error::JavaException) - } else { - Ok(2) - } - }) - .catch("java/lang/IndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - if rethrow { - env.throw(caught).unwrap(); - Err(Error::JavaException) - } else { - Ok(3) - } - }) - .catch("java/lang/StringIndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - Ok(4) - }) + .catch( + env, + jni_str!("java/lang/ArrayIndexOutOfBoundsException"), + |env, caught| { + assert!(!env.exception_check()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + if rethrow { + Err(Error::JavaException) + } else { + Ok(2) + } + }, + ) + .catch( + env, + jni_str!("java/lang/IndexOutOfBoundsException"), + |env, caught| { + assert!(!env.exception_check()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + if rethrow { + let _ = env.throw(&caught); + Err(Error::JavaException) + } else { + Ok(3) + } + }, + ) + .catch( + env, + jni_str!("java/lang/StringIndexOutOfBoundsException"), + |env, caught| { + assert!(!env.exception_check()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + Ok(4) + }, + ) .result() } #[test] fn test_catch_first() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { assert_eq!( test_catch( - &env, + env, Some("java/lang/IllegalArgumentException"), Err(Error::JavaException), false, @@ -278,16 +318,18 @@ mod test { .unwrap(), 1 ); - assert!(!env.exception_check().unwrap()); - }); + assert!(!env.exception_check()); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_second() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { assert_eq!( test_catch( - &env, + env, Some("java/lang/ArrayIndexOutOfBoundsException"), Err(Error::JavaException), false, @@ -295,16 +337,18 @@ mod test { .unwrap(), 2 ); - assert!(!env.exception_check().unwrap()); - }); + assert!(!env.exception_check()); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_third() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { assert_eq!( test_catch( - &env, + env, Some("java/lang/StringIndexOutOfBoundsException"), Err(Error::JavaException), false, @@ -312,294 +356,357 @@ mod test { .unwrap(), 3 ); - assert!(!env.exception_check().unwrap()); - }); + assert!(!env.exception_check()); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_ok() { - test_utils::JVM_ENV.with(|env| { - assert_eq!(test_catch(&env, None, Ok(0), false).unwrap(), 0); - assert!(!env.exception_check().unwrap()); - }); + test_utils::with_env(|env| { + assert_eq!(test_catch(env, None, Ok(0), false).unwrap(), 0); + assert!(!env.exception_check()); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_none() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/SecurityException"), Err(Error::JavaException), false, ) .unwrap_err() { - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( - env.is_instance_of(ex, "java/lang/SecurityException") + env.is_instance_of(&ex, jni_str!("java/lang/SecurityException")) .unwrap() ); } else { panic!("No JavaException"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_other() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { if let Error::InvalidCtorReturn = test_catch(env, None, Err(Error::InvalidCtorReturn), false).unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("InvalidCtorReturn not found"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_bogus_exception() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { if let Error::JavaException = test_catch(env, None, Err(Error::JavaException), false).unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("JavaException not found"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_prior_exception() { - test_utils::JVM_ENV.with(|env| { - let ex: JThrowable = env - .new_object("java/lang/IllegalArgumentException", "()V", &[]) - .unwrap() - .into(); - env.throw(ex).unwrap(); + test_utils::with_env(|env| { + let obj = env + .new_object( + jni_str!("java/lang/IllegalArgumentException"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let ex = env.cast_local::(obj).unwrap(); + let _ = env.throw(&ex); - if let Error::JavaException = test_catch(&env, None, Ok(0), false).unwrap_err() { - assert!(env.exception_check().unwrap()); + if let Error::JavaException = test_catch(env, None, Ok(0), false).unwrap_err() { + assert!(env.exception_check()); let actual_ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); - assert!(env.is_same_object(actual_ex, ex).unwrap()); + env.exception_clear(); + assert!(env.is_same_object(&actual_ex, &ex).unwrap()); } else { panic!("JavaException not found"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_rethrow() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/StringIndexOutOfBoundsException"), Err(Error::JavaException), true, ) .unwrap_err() { - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( - env.is_instance_of(ex, "java/lang/StringIndexOutOfBoundsException") + env.is_instance_of(&ex, jni_str!("java/lang/StringIndexOutOfBoundsException")) .unwrap() ); } else { panic!("JavaException not found"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_catch_bogus_rethrow() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/ArrayIndexOutOfBoundsException"), Err(Error::JavaException), true, ) .unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("JavaException not found"); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_panic_exception_static_str() { - test_utils::JVM_ENV.with(|env| { - use jni::{objects::JString, strings::JavaStr}; + test_utils::with_env(|env| { + use jni::objects::JString; - const STATIC_MSG: &'static str = "This is a &'static str"; + const STATIC_MSG: &str = "This is a &'static str"; let ex = super::JPanicException::new(env, Box::new(STATIC_MSG)).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::<&str>().unwrap(), STATIC_MSG); } - let msg: JString = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + let msg_obj = env + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() - .unwrap() - .into(); - let str = JavaStr::from_env(env, msg).unwrap(); - assert_eq!(str.to_str().unwrap(), STATIC_MSG); - }); + .unwrap(); + let msg = env.cast_local::(msg_obj).unwrap(); + let chars = msg.mutf8_chars(env).unwrap(); + assert_eq!(String::from(chars), STATIC_MSG); + Ok(()) + }) + .unwrap(); } #[test] fn test_panic_exception_string() { - test_utils::JVM_ENV.with(|env| { - use jni::{objects::JString, strings::JavaStr}; + test_utils::with_env(|env| { + use jni::objects::JString; use std::any::Any; - const STRING_MSG: &'static str = "This is a String"; + const STRING_MSG: &str = "This is a String"; let ex = super::JPanicException::new(env, Box::new(STRING_MSG.to_string())).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::().unwrap(), STRING_MSG); } - let msg: JString = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + let msg_obj = env + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() - .unwrap() - .into(); - let str = JavaStr::from_env(env, msg).unwrap(); - assert_eq!(str.to_str().unwrap(), STRING_MSG); + .unwrap(); + let msg = env.cast_local::(msg_obj).unwrap(); + let chars = msg.mutf8_chars(env).unwrap(); + assert_eq!(String::from(chars), STRING_MSG); - let any: Box = ex.take().unwrap(); + let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), STRING_MSG); - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_panic_exception_other() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { use jni::objects::JObject; - use std::{any::Any, convert::TryInto}; + use std::any::Any; let ex = super::JPanicException::new(env, Box::new(42)).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::().unwrap(), 42); } let msg = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() .unwrap(); - assert!(env.is_same_object(msg, JObject::null()).unwrap()); + assert!(env.is_same_object(&msg, JObject::null()).unwrap()); - let any: Box = ex.try_into().unwrap(); + let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), 42); - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_throw_unwind_ok() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let result = super::throw_unwind(env, || 42).unwrap(); assert_eq!(result, 42); - assert!(!env.exception_check().unwrap()); - }); + assert!(!env.exception_check()); + Ok(()) + }) + .unwrap(); } #[test] fn test_throw_unwind_panic() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( - env.is_instance_of(ex, "io/github/gedgygedgy/rust/panic/PanicException") - .unwrap() + env.is_instance_of( + &ex, + jni_str!("io/github/gedgygedgy/rust/panic/PanicException") + ) + .unwrap() ); let suppressed_list = env - .call_method(ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method( + &ex, + jni_str!("getSuppressed"), + jni_sig!("()[Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); - assert_eq!( - env.get_array_length(suppressed_list.into_inner()).unwrap(), - 0 - ); + let suppressed_array = unsafe { + jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) + }; + assert_eq!(suppressed_array.len(env).unwrap(), 0); - let ex = super::JPanicException::from_env(env, ex).unwrap(); - let any = ex.take().unwrap(); + let ex = super::JPanicException::from_env(ex); + let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_throw_unwind_panic_suppress() { - test_utils::JVM_ENV.with(|env| { - let old_ex: JThrowable = env - .new_object("java/lang/Exception", "()V", &[]) - .unwrap() - .into(); - env.throw(old_ex).unwrap(); + test_utils::with_env(|env| { + let obj = env + .new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]) + .unwrap(); + let old_ex = env.cast_local::(obj).unwrap(); + let _ = env.throw(&old_ex); super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( - env.is_instance_of(ex, "io/github/gedgygedgy/rust/panic/PanicException") - .unwrap() + env.is_instance_of( + &ex, + jni_str!("io/github/gedgygedgy/rust/panic/PanicException") + ) + .unwrap() ); let suppressed_list = env - .call_method(ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method( + &ex, + jni_str!("getSuppressed"), + jni_sig!("()[Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); - assert_eq!( - env.get_array_length(suppressed_list.into_inner()).unwrap(), - 1 - ); - let suppressed_ex = env - .get_object_array_element(suppressed_list.into_inner(), 0) - .unwrap(); - assert!(env.is_same_object(old_ex, suppressed_ex).unwrap()); - - let ex = super::JPanicException::from_env(env, ex).unwrap(); - let any = ex.take().unwrap(); + let suppressed_array = unsafe { + jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) + }; + assert_eq!(suppressed_array.len(env).unwrap(), 1); + let suppressed_ex = suppressed_array.get_element(env, 0).unwrap(); + assert!(env.is_same_object(&old_ex, &suppressed_ex).unwrap()); + + let ex = super::JPanicException::from_env(ex); + let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); - }); + Ok(()) + }) + .unwrap(); } #[test] #[should_panic(expected = "This is a panic")] fn test_panic_exception_resume_unwind() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let ex = super::JPanicException::new(env, Box::new("This is a panic")).unwrap(); - ex.resume_unwind().unwrap(); - }); + ex.resume_unwind(env).unwrap(); + Ok(()) + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 34587a80..f66b9c30 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,159 +1,85 @@ -use super::task::JPollResult; +#![allow(dead_code)] + use ::jni::{ - JNIEnv, JavaVM, - errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject}, - signature::JavaType, + Env, JavaVM, bind_java_type, + errors::Result, + jni_sig, jni_str, + objects::{Global, JObject}, }; use static_assertions::assert_impl_all; use std::{ - convert::TryFrom, future::Future, pin::Pin, task::{Context, Poll}, }; -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.future.Future`. Implements -/// [`Future`](std::future::Future) to allow asynchronous Rust code to wait for -/// a result from Java code. -/// -/// For a [`Send`] version of this, use [`JSendFuture`]. -pub struct JFuture<'a: 'b, 'b> { - internal: JObject<'a>, - poll: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JFuture<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let poll = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/future/Future") - .unwrap() - .as_obj(), - ), - "poll", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", - )?; - Ok(Self { - internal: obj, - poll, - env, - }) - } - - pub fn poll(&self, waker: JObject<'a>) -> Result> { - let result = self - .env - .call_method_unchecked( - self.internal, - self.poll, - JavaType::Object("io/github/gedgygedgy/rust/task/PollResult".into()), - &[waker.into()], - )? - .l()?; - JPollResult::from_env(self.env, result) - } - - pub fn into_future(self) -> JFutureIntoFuture<'a, 'b> { - JFutureIntoFuture(self) - } -} - -impl<'a: 'b, 'b> ::std::ops::Deref for JFuture<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JFuture<'a, 'b>) -> JObject<'a> { - other.internal - } -} - -pub struct JFutureIntoFuture<'a: 'b, 'b>(JFuture<'a, 'b>); - -impl<'a: 'b, 'b> JFutureIntoFuture<'a, 'b> { - fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { - use super::task::waker; - let result = self.0.poll(waker(self.0.env, context.waker().clone())?)?; - Ok( - if self.0.env.is_same_object(result.clone(), JObject::null())? { - Poll::Pending - } else { - Poll::Ready(result) - }, - ) - } -} - -impl<'a: 'b, 'b> Future for JFutureIntoFuture<'a, 'b> { - type Output = Result>; - - fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { - match self.poll_internal(context) { - Ok(Poll::Ready(result)) => Poll::Ready(Ok(result)), - Ok(Poll::Pending) => Poll::Pending, - Err(err) => Poll::Ready(Err(err)), - } - } +bind_java_type! { + pub JFuture => io.github.gedgygedgy.rust.future.Future, } -impl<'a: 'b, 'b> From> for JFuture<'a, 'b> { - fn from(fut: JFutureIntoFuture<'a, 'b>) -> Self { - fut.0 +impl<'local> JFuture<'local> { + pub fn poll(&self, env: &mut Env<'local>, waker: &JObject<'local>) -> Result> { + env.call_method( + self, + jni_str!("poll"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), + &[waker.into()], + )?.l() } } -impl<'a: 'b, 'b> std::ops::Deref for JFutureIntoFuture<'a, 'b> { - type Target = JFuture<'a, 'b>; - - fn deref(&self) -> &Self::Target { - &self.0 - } +bind_java_type! { + pub JFutureException => io.github.gedgygedgy.rust.future.FutureException, } -/// [`Send`] version of [`JFuture`]. pub struct JSendFuture { - internal: GlobalRef, + internal: Global>, vm: JavaVM, } -impl<'a: 'b, 'b> TryFrom> for JSendFuture { - type Error = Error; +impl JSendFuture { + pub fn new(env: &mut Env, future: &JFuture) -> Result { + Ok(Self { + internal: env.new_global_ref(&**future)?, + vm: env.get_java_vm()?, + }) + } - fn try_from(future: JFuture<'a, 'b>) -> Result { + pub fn from_env(env: &mut Env, obj: &JObject) -> Result { Ok(Self { - internal: future.env.new_global_ref(future.internal)?, - vm: future.env.get_java_vm()?, + internal: env.new_global_ref(obj)?, + vm: env.get_java_vm()?, + }) + } + + fn poll_internal( + &self, + context: &mut Context<'_>, + ) -> Result>>>> { + self.vm.attach_current_thread(|env| { + let jwaker = super::task::waker(env, context.waker().clone())?; + let local = env.new_local_ref(self.internal.as_obj())?; + let jfuture = env.cast_local::(local)?; + let result = jfuture.poll(env, &jwaker)?; + Ok(if env.is_same_object(&result, JObject::null())? { + Poll::Pending + } else { + Poll::Ready(Ok(env.new_global_ref(result)?)) + }) }) } } impl ::std::ops::Deref for JSendFuture { - type Target = GlobalRef; + type Target = Global>; fn deref(&self) -> &Self::Target { &self.internal } } -impl JSendFuture { - fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { - let env = self.vm.get_env()?; - let jfuture = JFuture::from_env(&env, self.internal.as_obj())?.into_future(); - jfuture - .poll_internal(context) - .map(|result| result.map(|result| Ok(env.new_global_ref(result)?))) - } -} - impl Future for JSendFuture { - type Output = Result; + type Output = Result>>; fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { match self.poll_internal(context) { @@ -167,8 +93,9 @@ assert_impl_all!(JSendFuture: Send); #[cfg(test)] mod test { - use super::super::{task::JPollResult, test_utils}; + use super::super::test_utils; use super::{JFuture, JSendFuture}; + use jni::{jni_sig, jni_str}; use std::{ future::Future, pin::Pin, @@ -177,9 +104,10 @@ mod test { #[test] fn test_jfuture() { + use super::super::task::JPollResult; use std::sync::Arc; - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -189,9 +117,15 @@ mod test { assert_eq!(data.value(), false); let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let mut future = JFuture::from_env(env, future_obj).unwrap().into_future(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); + let mut future = JSendFuture::new(env, &jfuture).unwrap(); assert!( Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)).is_pending() @@ -205,18 +139,26 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) .unwrap(); + env.call_method( + &future_obj, + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); let poll = Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)); if let Poll::Ready(result) = poll { - assert!( - env.is_same_object(result.unwrap().get().unwrap(), obj) - .unwrap() - ); + let global = result.unwrap(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { panic!("Poll result should be ready"); } @@ -225,46 +167,77 @@ mod test { let poll = Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)); if let Poll::Ready(result) = poll { - assert!( - env.is_same_object(result.unwrap().get().unwrap(), obj) - .unwrap() - ); + let global = result.unwrap(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { panic!("Poll result should be ready"); } assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); - }); + + Ok(()) + }) + .unwrap(); } #[test] fn test_jfuture_await() { + use super::super::task::JPollResult; use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - - block_on(async { - join!( - async { - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) - .unwrap(); - }, - async { - assert!( - env.is_same_object( - future.into_future().await.unwrap().get().unwrap(), - obj - ) - .unwrap() - ); - } - ); - }); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }) + .unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + ) + .unwrap(); + Ok(()) + }) + .unwrap(); + }, + async { + let global = future.await.unwrap(); + test_utils::with_env(|env| { + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); + Ok(()) + }) + .unwrap(); + } + ); }); } @@ -272,66 +245,186 @@ mod test { fn test_jfuture_await_throw() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { + let (future, future_obj_global, ex_global) = test_utils::with_env(|env| { let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let ex = env.new_object("java/lang/Exception", "()V", &[]).unwrap(); - - block_on(async { - join!( - async { + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let ex = env + .new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]) + .unwrap(); + let ex_global = env.new_global_ref(&ex).unwrap(); + Ok((future, future_obj_global, ex_global)) + }) + .unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); env.call_method( - future_obj, - "wakeWithThrowable", - "(Ljava/lang/Throwable;)V", - &[ex.into()], + &future_local, + jni_str!("wakeWithThrowable"), + jni_sig!("(Ljava/lang/Throwable;)V"), + &[(&ex_local).into()], ) .unwrap(); - }, - async { - future.into_future().await.unwrap().get().unwrap_err(); + Ok(()) + }) + .unwrap(); + }, + async { + use super::super::task::JPollResult; + + let global = future.await.unwrap(); + test_utils::with_env(|env| { + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); + let _err = poll_result.get(env).unwrap_err(); + let future_ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); let actual_ex = env - .call_method(future_ex, "getCause", "()Ljava/lang/Throwable;", &[]) + .call_method( + &future_ex, + jni_str!("getCause"), + jni_sig!("()Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); - assert!(env.is_same_object(actual_ex, ex).unwrap()); - } - ); - }); + let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); + assert!(env.is_same_object(&actual_ex, &ex_local).unwrap()); + Ok(()) + }) + .unwrap(); + } + ); }); } + #[test] + fn test_jsendfuture_cross_thread_await() { + use super::super::task::JPollResult; + use futures::executor::block_on; + use std::sync::{Arc, Barrier, mpsc}; + + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { + let future_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future = JSendFuture::from_env(env, &future_obj).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }) + .unwrap(); + + let barrier = Arc::new(Barrier::new(2)); + let (tx, rx) = mpsc::channel(); + let worker_barrier = barrier.clone(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + let global = block_on(future).unwrap(); + tx.send(global).unwrap(); + }); + + barrier.wait(); + test_utils::with_env(|env| { + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + )?; + Ok(()) + }) + .unwrap(); + worker.join().unwrap(); + let global = rx.recv().unwrap(); + test_utils::with_env(|env| { + let actual = env.new_local_ref(global.as_obj()).unwrap(); + let poll = env.cast_local::(actual).unwrap(); + let result = poll.get(env).unwrap(); + let expected = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result, &expected).unwrap()); + Ok(()) + }) + .unwrap(); + } + #[test] fn test_jsendfuture_await() { + use super::super::task::JPollResult; use futures::{executor::block_on, join}; - use std::convert::TryInto; - test_utils::JVM_ENV.with(|env| { + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let future: JSendFuture = future.try_into().unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - - block_on(async { - join!( - async { - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) - .unwrap(); - }, - async { - let global_ref = future.await.unwrap(); - let jpoll = JPollResult::from_env(env, global_ref.as_obj()).unwrap(); - assert!(env.is_same_object(jpoll.get().unwrap(), obj).unwrap()); - } - ); - }); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future = JSendFuture::from_env(env, &future_obj).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }) + .unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + ) + .unwrap(); + Ok(()) + }) + .unwrap(); + }, + async { + let global_ref = future.await.unwrap(); + test_utils::with_env(|env| { + let local = env.new_local_ref(global_ref.as_obj()).unwrap(); + let jpoll = env.cast_local::(local).unwrap(); + let result_obj = jpoll.get(env).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); + Ok(()) + }) + .unwrap(); + } + ); }); } } diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 84bae6b2..795f939f 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -1,5 +1,4 @@ pub mod arrays; -pub mod classcache; pub mod exceptions; pub mod future; pub mod ops; @@ -9,46 +8,56 @@ pub mod uuid; #[cfg(test)] pub(crate) mod test_utils { - use jni::{JNIEnv, JavaVM, objects::GlobalRef}; + use jni::{ + Env, JavaVM, NativeMethod, jni_sig, jni_str, + objects::{Global, JObject, Reference}, + }; use lazy_static::lazy_static; use std::{ + cell::Cell, + ffi::c_void, sync::{Arc, Mutex}, task::{Wake, Waker}, }; - use jni::NativeMethod; - - fn test_init(env: &JNIEnv) -> jni::errors::Result<()> { - use std::ffi::c_void; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/Future")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/FutureException")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnAdapter")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/stream/Stream")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/stream/StreamPoll")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/Waker")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/PollResult")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnRunnableImpl")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnFunctionImpl")?; - - let class = env.auto_local(env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?); - env.register_native_methods( - &class, + fn test_init(env: &mut Env) -> jni::errors::Result<()> { + use super::{ + future::{JFuture, JFutureException}, + ops::{JFnAdapter, JFnBiFunctionImpl, JFnFunctionImpl, JFnRunnableImpl}, + stream::{JStream, JStreamPoll}, + task::{JPollResult, JWaker}, + }; + + let loader = jni::objects::LoaderContext::default(); + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + + let fn_adapter_class = ::lookup_class(env, &loader)?; + unsafe { + env.register_native_methods( + &*fn_adapter_class, &[ - NativeMethod { - name: "callInternal".into(), - sig: - "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" - .into(), - fn_ptr: super::ops::fn_adapter_call_internal as *mut c_void, - }, - NativeMethod { - name: "closeInternal".into(), - sig: "()V".into(), - fn_ptr: super::ops::fn_adapter_close_internal as *mut c_void, - }, + NativeMethod::from_raw_parts( + jni_str!("callInternal"), + jni_str!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), + super::ops::fn_adapter_call_internal as *mut c_void, + ), + NativeMethod::from_raw_parts( + jni_str!("closeInternal"), + jni_str!("()V"), + super::ops::fn_adapter_close_internal as *mut c_void, + ), ], - )?; + )? + }; Ok(()) } @@ -85,32 +94,37 @@ pub(crate) mod test_utils { struct GlobalJVM { jvm: JavaVM, - class_loader: GlobalRef, + class_loader: Global>, } thread_local! { - pub static JVM_ENV: JNIEnv<'static> = { - let env = JVM.jvm.attach_current_thread_permanently().unwrap(); - - let thread = env - .call_static_method( - "java/lang/Thread", - "currentThread", - "()Ljava/lang/Thread;", - &[], - ) - .unwrap() - .l() - .unwrap(); - env.call_method( - thread, - "setContextClassLoader", - "(Ljava/lang/ClassLoader;)V", - &[JVM.class_loader.as_obj().into()] - ).unwrap(); - - env - } + static CLASS_LOADER_SET: Cell = const { Cell::new(false) }; + } + + pub fn with_env(f: F) -> jni::errors::Result + where + F: FnOnce(&mut Env) -> jni::errors::Result, + { + JVM.jvm.attach_current_thread(|env| { + if !CLASS_LOADER_SET.with(|c| c.get()) { + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + )? + .l()?; + env.call_method( + &thread, + jni_str!("setContextClassLoader"), + jni_sig!("(Ljava/lang/ClassLoader;)V"), + &[JVM.class_loader.as_obj().into()], + )?; + CLASS_LOADER_SET.with(|c| c.set(true)); + } + f(env) + }) } lazy_static! { @@ -125,41 +139,77 @@ pub(crate) mod test_utils { jni_utils_jar.push("libs"); jni_utils_jar.push("btleplug-jni.jar"); - let jvm_args = InitArgsBuilder::new() - .option(&format!( - "-Djava.class.path={}", - jni_utils_jar.to_str().unwrap() - )) - .build() - .unwrap(); + let classpath = format!("-Djava.class.path={}", jni_utils_jar.to_str().unwrap()); + let jvm_args = InitArgsBuilder::new().option(&classpath).build().unwrap(); let jvm = JavaVM::new(jvm_args).unwrap(); - let env = jvm.attach_current_thread_permanently().unwrap(); - test_init(&env).unwrap(); - - let thread = env - .call_static_method( - "java/lang/Thread", - "currentThread", - "()Ljava/lang/Thread;", - &[], - ) - .unwrap() - .l() + let class_loader = jvm + .attach_current_thread(|env| { + test_init(env).unwrap(); + + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + let class_loader = env + .call_method( + &thread, + jni_str!("getContextClassLoader"), + jni_sig!("()Ljava/lang/ClassLoader;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + Ok::<_, jni::errors::Error>(env.new_global_ref(class_loader).unwrap()) + }) .unwrap(); - let class_loader = env - .call_method( - thread, - "getContextClassLoader", - "()Ljava/lang/ClassLoader;", - &[], - ) - .unwrap() - .l() - .unwrap(); - let class_loader = env.new_global_ref(class_loader).unwrap(); GlobalJVM { jvm, class_loader } }; } + + #[test] + fn with_env_is_safe_across_threads() { + use std::sync::{Arc, Barrier, mpsc}; + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = barrier.clone(); + let (tx, rx) = mpsc::channel(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + with_env(|env| { + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + )? + .l()?; + let name = env + .call_method( + &thread, + jni_str!("getName"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + let name = env.cast_local::(name)?.to_string(); + tx.send(name).unwrap(); + Ok(()) + }) + .unwrap(); + }); + + barrier.wait(); + worker.join().unwrap(); + assert!(!rx.recv().unwrap().is_empty()); + } } diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 3b896251..245ca2e8 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,10 +1,30 @@ +#![allow(dead_code)] + +use ::jni::errors::ThrowRuntimeExAndDefault; use ::jni::{ - JNIEnv, + Env, EnvUnowned, bind_java_type, errors::Result, - objects::{JClass, JObject}, + jni_sig, jni_str, + objects::{JObject, Reference}, }; use std::sync::{Arc, Mutex}; +bind_java_type! { + pub JFnAdapter => io.github.gedgygedgy.rust.ops.FnAdapter, +} + +bind_java_type! { + pub JFnRunnableImpl => io.github.gedgygedgy.rust.ops.FnRunnableImpl, +} + +bind_java_type! { + pub JFnBiFunctionImpl => io.github.gedgygedgy.rust.ops.FnBiFunctionImpl, +} + +bind_java_type! { + pub JFnFunctionImpl => io.github.gedgygedgy.rust.ops.FnFunctionImpl, +} + macro_rules! define_fn_adapter { ( fn_once: $fo:ident, @@ -16,7 +36,7 @@ macro_rules! define_fn_adapter { fn: $f:ident, fn_local: $fl:ident, fn_internal: $fi:ident, - impl_class: $ic:literal, + impl_type: $it:ty, doc_class: $dc:literal, doc_method: $dm:literal, doc_fn_once: $dfo:literal, @@ -25,89 +45,96 @@ macro_rules! define_fn_adapter { signature: $closure_name:ident: impl for<'c, 'd> Fn$args:tt -> $ret:ty, closure: $closure:expr, ) => { - fn $foi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(clippy::unused_unit)] + fn $foi<'local>( + env: &mut Env<'local>, $closure_name: impl for<'c, 'd> FnOnce$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_once_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_once_adapter(env, $closure, local)?; + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + &*class, + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } - pub fn $fo<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(clippy::unused_unit)] + pub fn $fo<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + Send + 'static, - ) -> Result> { + ) -> Result> { $foi(env, f, false) } - #[allow(dead_code)] - pub fn $fol<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(dead_code, clippy::unused_unit)] + pub fn $fol<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $foi(env, f, true) } - fn $fmi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(clippy::unused_unit)] + fn $fmi<'local>( + env: &mut Env<'local>, mut $closure_name: impl for<'c, 'd> FnMut$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_mut_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_mut_adapter(env, $closure, local)?; + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + &*class, + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } - #[allow(dead_code)] - pub fn $fm<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(dead_code, clippy::unused_unit)] + pub fn $fm<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + Send + 'static, - ) -> Result> { + ) -> Result> { $fmi(env, f, false) } - #[allow(dead_code)] - pub fn $fml<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(dead_code, clippy::unused_unit)] + pub fn $fml<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $fmi(env, f, true) } - fn $fi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(clippy::unused_unit)] + fn $fi<'local>( + env: &mut Env<'local>, $closure_name: impl for<'c, 'd> Fn$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_adapter(env, $closure, local)?; + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + &*class, + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } - #[allow(dead_code)] - pub fn $f<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(dead_code, clippy::unused_unit)] + pub fn $f<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + Send + Sync + 'static, - ) -> Result> { + ) -> Result> { $fi(env, f, false) } - #[allow(dead_code)] - pub fn $fl<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + #[allow(dead_code, clippy::unused_unit)] + pub fn $fl<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $fi(env, f, true) } }; @@ -123,13 +150,13 @@ define_fn_adapter! { fn: fn_runnable, fn_local: fn_runnable_local, fn_internal: fn_runnable_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnRunnableImpl", + impl_type: JFnRunnableImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnRunnable", doc_method: "run()", doc_fn_once: "fn_once_runnable", doc_fn: "fn_runnable", doc_noop: "be a no-op", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>) -> (), + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>) -> (), closure: move |env, _obj1, obj2, _arg1, _arg2| { f(env, obj2); JObject::null() @@ -146,13 +173,13 @@ define_fn_adapter! { fn: fn_bi_function, fn_local: fn_bi_function_local, fn_internal: fn_bi_function_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl", + impl_type: JFnBiFunctionImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnBiFunction", doc_method: "apply()", doc_fn_once: "fn_once_bi_function", doc_fn: "fn_bi_funciton", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, arg2| { f(env, obj2, arg1, arg2) }, @@ -168,13 +195,13 @@ define_fn_adapter! { fn: fn_function, fn_local: fn_function_local, fn_internal: fn_function_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnFunctionImpl", + impl_type: JFnFunctionImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnFunction", doc_method: "apply()", doc_fn_once: "fn_once_function", doc_fn: "fn_function", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, _arg2| { f(env, obj2, arg1) }, @@ -188,7 +215,7 @@ unsafe impl Sync for SendSyncWrapper {} type FnWrapper = SendSyncWrapper< Arc< dyn for<'a, 'b> Fn( - &'b JNIEnv<'a>, + &'b mut Env<'a>, JObject<'a>, JObject<'a>, JObject<'a>, @@ -198,10 +225,10 @@ type FnWrapper = SendSyncWrapper< >, >; -fn fn_once_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +fn fn_once_adapter<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce( - &'d JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -209,7 +236,7 @@ fn fn_once_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let mutex = Mutex::new(Some(f)); fn_adapter( env, @@ -228,10 +255,10 @@ fn fn_once_adapter<'a: 'b, 'b>( ) } -fn fn_mut_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +fn fn_mut_adapter<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut( - &'d JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -239,7 +266,7 @@ fn fn_mut_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let mutex = Mutex::new(f); fn_adapter( env, @@ -251,10 +278,11 @@ fn fn_mut_adapter<'a: 'b, 'b>( ) } -fn fn_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +#[allow(clippy::type_complexity)] +fn fn_adapter<'local>( + env: &mut Env<'local>, f: impl for<'c, 'd> Fn( - &'d JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -262,10 +290,10 @@ fn fn_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let arc: Arc< dyn for<'c, 'd> Fn( - &'d JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -273,39 +301,53 @@ fn fn_adapter<'a: 'b, 'b>( ) -> JObject<'c>, > = Arc::from(f); - let obj = env.new_object( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter") - .unwrap() - .as_obj(), - ), - "(Z)V", - &[local.into()], - )?; - env.set_rust_field::<_, _, FnWrapper>(obj, "data", SendSyncWrapper(arc))?; + let class = ::lookup_class(env, &Default::default())?; + let obj = env.new_object(&*class, jni_sig!("(Z)V"), &[local.into()])?; + unsafe { env.set_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data"), SendSyncWrapper(arc)) }?; Ok(obj) } -pub(crate) extern "C" fn fn_adapter_call_internal<'a>( - env: JNIEnv<'a>, - obj1: JObject<'a>, - obj2: JObject<'a>, - arg1: JObject<'a>, - arg2: JObject<'a>, -) -> JObject<'a> { - use std::panic::AssertUnwindSafe; +pub(crate) extern "C" fn fn_adapter_call_internal<'local>( + mut env: EnvUnowned<'local>, + obj1: JObject<'local>, + obj2: JObject<'local>, + arg1: JObject<'local>, + arg2: JObject<'local>, +) -> JObject<'local> { + use std::panic::{AssertUnwindSafe, catch_unwind}; - let arc = if let Ok(f) = env.get_rust_field::<_, _, FnWrapper>(obj1, "data") { - AssertUnwindSafe(f.0.clone()) - } else { - return JObject::null(); - }; - super::exceptions::throw_unwind(&env, || arc(&env, obj1, obj2, arg1, arg2)) - .unwrap_or_else(|_| JObject::null()) + env.with_env( + |env| -> std::result::Result, jni::errors::Error> { + let arc = if let Ok(f) = + unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } + { + AssertUnwindSafe(f.0.clone()) + } else { + return Ok(JObject::null()); + }; + match catch_unwind(AssertUnwindSafe(|| arc(env, obj1, obj2, arg1, arg2))) { + Ok(result) => Ok(result), + Err(panic) => { + let _ = super::exceptions::throw_panic(env, panic); + Ok(JObject::null()) + } + } + }, + ) + .resolve::() } -pub(crate) extern "C" fn fn_adapter_close_internal(env: JNIEnv, obj: JObject) { - let _ = super::exceptions::throw_unwind(&env, || { - let _ = env.take_rust_field::<_, _, FnWrapper>(obj, "data"); - }); +pub(crate) extern "C" fn fn_adapter_close_internal(mut env: EnvUnowned, obj: JObject) { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + env.with_env(|env| { + let result = catch_unwind(AssertUnwindSafe(|| { + let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data")) }; + })); + if let Err(panic) = result { + super::exceptions::throw_panic(env, panic)?; + } + Ok::<(), jni::errors::Error>(()) + }) + .resolve::(); } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index c108247c..9249c4b2 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,145 +1,103 @@ +#![allow(dead_code)] + use super::task::JPollResult; use ::jni::{ - JNIEnv, JavaVM, - errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject}, - signature::JavaType, + Env, JavaVM, bind_java_type, + errors::Result, + jni_sig, jni_str, + objects::{Global, JObject}, }; use futures::stream::Stream; use static_assertions::assert_impl_all; use std::{ - convert::TryFrom, pin::Pin, task::{Context, Poll}, }; -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.stream.Stream`. -/// -/// For a [`Send`] version of this, use [`JSendStream`]. -pub struct JStream<'a: 'b, 'b> { - internal: JObject<'a>, - poll_next: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JStream => io.github.gedgygedgy.rust.stream.Stream, } -impl<'a: 'b, 'b> JStream<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let poll_next = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream") - .unwrap() - .as_obj(), - ), - "pollNext", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", - )?; - Ok(Self { - internal: obj, - poll_next, - env, - }) - } - - fn j_poll_next(&self, waker: JObject<'a>) -> Result>>> { - let result = self - .env - .call_method_unchecked( - self.internal, - self.poll_next, - JavaType::Object("io/github/gedgygedgy/rust/task/PollResult".to_string()), - &[waker.into()], - )? - .l()?; - let _auto_local = self.env.auto_local(result); - Ok(if self.env.is_same_object(result, JObject::null())? { - Poll::Pending - } else { - Poll::Ready({ - let poll = JPollResult::from_env(self.env, result)?; - let stream_poll_obj = poll.get()?; - if self.env.is_same_object(stream_poll_obj, JObject::null())? { - None - } else { - let stream_poll = JStreamPoll::from_env(self.env, stream_poll_obj)?; - Some(stream_poll.get()?) - } - }) - }) - } - - fn poll_next_internal(&self, context: &mut Context) -> Result>>> { - use super::task::waker; - self.j_poll_next(waker(self.env, context.waker().clone())?) +impl<'local> JStream<'local> { + pub fn poll_next( + &self, + env: &mut Env<'local>, + waker: &JObject<'local>, + ) -> Result> { + env.call_method( + self, + jni_str!("pollNext"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), + &[waker.into()], + )?.l() } } -impl<'a: 'b, 'b> ::std::ops::Deref for JStream<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } +bind_java_type! { + pub JStreamPoll => io.github.gedgygedgy.rust.stream.StreamPoll, + methods { + fn get() -> JObject, + }, } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JStream<'a, 'b>) -> JObject<'a> { - other.internal - } +pub struct JSendStream { + internal: Global>, + vm: JavaVM, } -impl<'a: 'b, 'b> Stream for JStream<'a, 'b> { - type Item = Result>; +impl JSendStream { + pub fn new(env: &mut Env, stream: &JStream) -> Result { + Ok(Self { + internal: env.new_global_ref(&**stream)?, + vm: env.get_java_vm()?, + }) + } - fn poll_next(self: Pin<&mut Self>, context: &mut Context) -> Poll> { - match self.poll_next_internal(context) { - Ok(Poll::Ready(result)) => Poll::Ready(result.map(|o| Ok(o))), - Ok(Poll::Pending) => Poll::Pending, - Err(err) => Poll::Ready(Some(Err(err))), - } + pub fn from_env(env: &mut Env, obj: &JObject) -> Result { + Ok(Self { + internal: env.new_global_ref(obj)?, + vm: env.get_java_vm()?, + }) } -} -/// [`Send`] version of [`JStream`]. -pub struct JSendStream { - internal: GlobalRef, - vm: JavaVM, -} + fn poll_next_internal( + &self, + context: &mut Context<'_>, + ) -> Result>>>>> { + self.vm.attach_current_thread(|env| { + let jwaker = super::task::waker(env, context.waker().clone())?; + let local = env.new_local_ref(self.internal.as_obj())?; + let jstream = env.cast_local::(local)?; + let result = jstream.poll_next(env, &jwaker)?; + + if env.is_same_object(&result, JObject::null())? { + return Ok(Poll::Pending); + } -impl<'a: 'b, 'b> TryFrom> for JSendStream { - type Error = Error; + let poll_result = env.cast_local::(result)?; + let stream_poll_obj = poll_result.get(env)?; - fn try_from(stream: JStream<'a, 'b>) -> Result { - Ok(Self { - internal: stream.env.new_global_ref(stream.internal)?, - vm: stream.env.get_java_vm()?, + if env.is_same_object(&stream_poll_obj, JObject::null())? { + return Ok(Poll::Ready(None)); + } + + let stream_poll = env.cast_local::(stream_poll_obj)?; + let obj = stream_poll.get(env)?; + Ok(Poll::Ready(Some(Ok(env.new_global_ref(obj)?)))) }) } } impl ::std::ops::Deref for JSendStream { - type Target = GlobalRef; + type Target = Global>; fn deref(&self) -> &Self::Target { &self.internal } } -impl JSendStream { - fn poll_next_internal( - &self, - context: &mut Context<'_>, - ) -> Result>>> { - let env = self.vm.get_env()?; - let jstream = JStream::from_env(&env, self.internal.as_obj())?; - jstream - .poll_next_internal(context) - .map(|result| result.map(|result| result.map(|obj| env.new_global_ref(obj)))) - } -} - impl Stream for JSendStream { - type Item = Result; + type Item = Result>>; fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { match self.poll_next_internal(context) { @@ -151,47 +109,12 @@ impl Stream for JSendStream { assert_impl_all!(JSendStream: Send); -struct JStreamPoll<'a: 'b, 'b> { - internal: JObject<'a>, - get: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JStreamPoll<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let get = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll") - .unwrap() - .as_obj(), - ), - "get", - "()Ljava/lang/Object;", - )?; - Ok(Self { - internal: obj, - get, - env, - }) - } - - pub fn get(&self) -> Result> { - self.env - .call_method_unchecked( - self.internal, - self.get, - JavaType::Object("java/lang/Object".into()), - &[], - )? - .l() - } -} - #[cfg(test)] mod test { use super::super::test_utils; - use super::JStream; + use super::{JSendStream, JStream}; use futures::stream::Stream; + use jni::{jni_sig, jni_str}; use std::{ pin::Pin, task::{Context, Poll}, @@ -201,7 +124,7 @@ mod test { fn test_jstream() { use std::sync::Arc; - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -211,9 +134,15 @@ mod test { assert_eq!(data.value(), false); let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let mut stream = JStream::from_env(env, stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = env.cast_local::(stream_local).unwrap(); + let mut stream = JSendStream::new(env, &jstream).unwrap(); assert!( Pin::new(&mut stream) @@ -223,23 +152,36 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) .unwrap(); + env.call_method( + &stream_obj, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj1).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) .unwrap(); + env.call_method( + &stream_obj, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj2).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - data.set_value(false); let poll = Pin::new(&mut stream).poll_next(&mut Context::from_waker(&waker)); if let Poll::Ready(Some(Ok(actual_obj1))) = poll { - assert!(env.is_same_object(actual_obj1, obj1).unwrap()); + assert!(env.is_same_object(actual_obj1.as_obj(), &obj1).unwrap()); } else { panic!("Poll result should be ready"); } @@ -248,7 +190,7 @@ mod test { let poll = Pin::new(&mut stream).poll_next(&mut Context::from_waker(&waker)); if let Poll::Ready(Some(Ok(actual_obj2))) = poll { - assert!(env.is_same_object(actual_obj2, obj2).unwrap()); + assert!(env.is_same_object(actual_obj2.as_obj(), &obj2).unwrap()); } else { panic!("Poll result should be ready"); } @@ -263,7 +205,8 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); + env.call_method(&stream_obj, jni_str!("finish"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); @@ -275,91 +218,222 @@ mod test { } assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - }); + + Ok(()) + }) + .unwrap(); } #[test] fn test_jstream_await() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { + let (mut stream, stream_obj_global, obj1_global, obj2_global) = + test_utils::with_env(|env| { + let stream_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = env.cast_local::(stream_local).unwrap(); + let stream = JSendStream::new(env, &jstream).unwrap(); + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }) + .unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { + let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + env.call_method( + &s, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&o1).into()], + ) + .unwrap(); + env.call_method( + &s, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&o2).into()], + ) + .unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]) + .unwrap(); + Ok(()) + }) + .unwrap(); + }, + async { + use futures::StreamExt; + let g1 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + Ok(()) + }) + .unwrap(); + + let g2 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + Ok(()) + }) + .unwrap(); + + assert!(stream.next().await.is_none()); + } + ); + }); + } + + #[test] + fn test_jsendstream_cross_thread_await() { + use futures::{StreamExt, executor::block_on}; + use std::sync::{Arc, Barrier, mpsc}; + + let (mut stream, stream_obj_global, obj_global) = test_utils::with_env(|env| { let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) .unwrap(); - let mut stream = JStream::from_env(env, stream_obj).unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - - block_on(async { - join!( - async { - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) - .unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) - .unwrap(); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); - }, - async { - use futures::StreamExt; - assert!( - env.is_same_object(stream.next().await.unwrap().unwrap(), obj1) - .unwrap() - ); - assert!( - env.is_same_object(stream.next().await.unwrap().unwrap(), obj2) - .unwrap() - ); - assert!(stream.next().await.is_none()); - } - ); - }); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((stream, stream_obj_global, obj_global)) + }) + .unwrap(); + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = barrier.clone(); + let (tx, rx) = mpsc::channel(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + let actual = block_on(stream.next()).unwrap().unwrap(); + tx.send(actual).unwrap(); }); + + barrier.wait(); + test_utils::with_env(|env| { + let stream_local = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &stream_local, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + )?; + Ok(()) + }) + .unwrap(); + worker.join().unwrap(); + let actual = rx.recv().unwrap(); + test_utils::with_env(|env| { + let expected = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(actual.as_obj(), &expected).unwrap()); + Ok(()) + }) + .unwrap(); } #[test] fn test_jsendstream_await() { - use super::JSendStream; use futures::{executor::block_on, join}; - use std::convert::TryInto; - test_utils::JVM_ENV.with(|env| { - let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) - .unwrap(); - let stream = JStream::from_env(env, stream_obj).unwrap(); - let mut stream: JSendStream = stream.try_into().unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - - block_on(async { - join!( - async { - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) - .unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) + let (mut stream, stream_obj_global, obj1_global, obj2_global) = + test_utils::with_env(|env| { + let stream_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }) + .unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { + let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + env.call_method( + &s, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&o1).into()], + ) + .unwrap(); + env.call_method( + &s, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&o2).into()], + ) + .unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]) .unwrap(); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); - }, - async { - use futures::StreamExt; - assert!( - env.is_same_object( - stream.next().await.unwrap().unwrap().as_obj(), - obj1 - ) - .unwrap() - ); - assert!( - env.is_same_object( - stream.next().await.unwrap().unwrap().as_obj(), - obj2 - ) - .unwrap() - ); - assert!(stream.next().await.is_none()); - } - ); - }); + Ok(()) + }) + .unwrap(); + }, + async { + use futures::StreamExt; + let g1 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + Ok(()) + }) + .unwrap(); + + let g2 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + Ok(()) + }) + .unwrap(); + + assert!(stream.next().await.is_none()); + } + ); }); } } diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index c0b3c038..8de5bc7b 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -1,87 +1,43 @@ use ::jni::{ - JNIEnv, + Env, bind_java_type, errors::Result, - objects::{JClass, JMethodID, JObject}, - signature::JavaType, + jni_sig, + objects::{JObject, Reference}, }; use std::task::Waker; -/// Wraps the given waker in a `io.github.gedgygedgy.rust.task.Waker` object. -pub fn waker<'a: 'b, 'b>(env: &'b JNIEnv<'a>, waker: Waker) -> Result> { +bind_java_type! { + pub JWaker => io.github.gedgygedgy.rust.task.Waker, +} + +pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { let runnable = super::ops::fn_once_runnable(env, |_e, _o| waker.wake())?; + let class = ::lookup_class(env, &Default::default())?; let obj = env.new_object( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker") - .unwrap() - .as_obj(), - ), - "(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V", - &[runnable.into()], + &*class, + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V"), + &[(&runnable).into()], )?; Ok(obj) } -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.task.PollResult`. -pub struct JPollResult<'a: 'b, 'b> { - internal: JObject<'a>, - get: JMethodID<'a>, - env: &'b JNIEnv<'a>, -} - -impl<'a: 'b, 'b> JPollResult<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let get = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult") - .unwrap() - .as_obj(), - ), - "get", - "()Ljava/lang/Object;", - )?; - Ok(Self { - internal: obj, - get, - env, - }) - } - - pub fn get(&self) -> Result> { - self.env - .call_method_unchecked( - self.internal, - self.get, - JavaType::Object("java/lang/Object".into()), - &[], - )? - .l() - } -} - -impl<'a: 'b, 'b> ::std::ops::Deref for JPollResult<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JPollResult<'a, 'b>) -> JObject<'a> { - other.internal - } +bind_java_type! { + pub JPollResult => io.github.gedgygedgy.rust.task.PollResult, + methods { + fn get() -> JObject, + }, } #[cfg(test)] mod test { use super::super::test_utils; + use jni::{jni_sig, jni_str}; use std::sync::Arc; #[test] fn test_waker_wake() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -94,20 +50,24 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), true); data.set_value(false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_waker_close_wake() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -120,13 +80,17 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(jwaker, "close", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("close"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - }); + Ok(()) + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index d5fdf6fa..c67c8e86 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,97 +1,40 @@ -use jni::{ - JNIEnv, - errors::Result, - objects::{AutoLocal, JMethodID, JObject}, - signature::{JavaType, Primitive}, - sys::jlong, -}; +use jni::{Env, bind_java_type, errors::Result, sys::jlong}; use uuid::Uuid; -/// Wrapper for [`JObject`]s that contain `java.util.UUID`. Provides methods -/// to convert to and from a [`Uuid`]. -pub struct JUuid<'a: 'b, 'b> { - internal: JObject<'a>, - get_least_significant_bits: JMethodID<'a>, - get_most_significant_bits: JMethodID<'a>, - env: &'b JNIEnv<'a>, +bind_java_type! { + pub JUuid => java.util.UUID, + constructors { + fn with_bits(most_significant_bits: jlong, least_significant_bits: jlong), + }, + methods { + fn get_least_significant_bits() -> jlong, + fn get_most_significant_bits() -> jlong, + }, } -impl<'a: 'b, 'b> JUuid<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("java/util/UUID")?); - Self::from_env_impl(env, obj, class) - } - - pub fn new(env: &'b JNIEnv<'a>, uuid: Uuid) -> Result { +impl JUuid<'_> { + pub fn new<'local>(env: &mut Env<'local>, uuid: Uuid) -> Result> { let val = uuid.as_u128(); let least = (val & 0xFFFFFFFFFFFFFFFF) as jlong; let most = ((val >> 64) & 0xFFFFFFFFFFFFFFFF) as jlong; - - let class = env.auto_local(env.find_class("java/util/UUID")?); - let obj = env.new_object(&class, "(JJ)V", &[most.into(), least.into()])?; - Self::from_env_impl(env, obj, class) + JUuid::with_bits(env, most, least) } +} - pub fn as_uuid(&self) -> Result { - let least = self - .env - .call_method_unchecked( - self.internal, - self.get_least_significant_bits, - JavaType::Primitive(Primitive::Long), - &[], - )? - .j()? as u64; - let most = self - .env - .call_method_unchecked( - self.internal, - self.get_most_significant_bits, - JavaType::Primitive(Primitive::Long), - &[], - )? - .j()? as u64; +impl<'local> JUuid<'local> { + pub fn as_uuid(&self, env: &mut Env<'local>) -> Result { + let least = self.get_least_significant_bits(env)? as u64; + let most = self.get_most_significant_bits(env)? as u64; let val = ((most as u128) << 64) | (least as u128); Ok(Uuid::from_u128(val)) } - - fn from_env_impl( - env: &'b JNIEnv<'a>, - obj: JObject<'a>, - class: AutoLocal<'a, 'b>, - ) -> Result { - let get_least_significant_bits = - env.get_method_id(&class, "getLeastSignificantBits", "()J")?; - let get_most_significant_bits = - env.get_method_id(&class, "getMostSignificantBits", "()J")?; - Ok(Self { - internal: obj, - get_least_significant_bits, - get_most_significant_bits, - env, - }) - } -} - -impl<'a: 'b, 'b> ::std::ops::Deref for JUuid<'a, 'b> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JUuid<'a, 'b>) -> JObject<'a> { - other.internal - } } #[cfg(test)] mod test { use super::super::test_utils; use super::JUuid; - use jni::{objects::JObject, sys::jlong}; + use jni::{jni_sig, jni_str, objects::JObject, sys::jlong}; use uuid::Uuid; struct UuidTest { @@ -115,7 +58,7 @@ mod test { #[test] fn test_uuid_new() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; @@ -124,35 +67,53 @@ mod test { let obj: JObject = uuid_obj.into(); let actual_most = env - .call_method(obj, "getMostSignificantBits", "()J", &[]) + .call_method( + &obj, + jni_str!("getMostSignificantBits"), + jni_sig!("()J"), + &[], + ) .unwrap() .j() .unwrap(); let actual_least = env - .call_method(obj, "getLeastSignificantBits", "()J", &[]) + .call_method( + &obj, + jni_str!("getLeastSignificantBits"), + jni_sig!("()J"), + &[], + ) .unwrap() .j() .unwrap(); assert_eq!(actual_most, most); assert_eq!(actual_least, least); } - }); + Ok(()) + }) + .unwrap(); } #[test] fn test_uuid_as_uuid() { - test_utils::JVM_ENV.with(|env| { + test_utils::with_env(|env| { for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; let obj = env - .new_object("java/util/UUID", "(JJ)V", &[most.into(), least.into()]) + .new_object( + jni_str!("java/util/UUID"), + jni_sig!("(JJ)V"), + &[most.into(), least.into()], + ) .unwrap(); - let uuid_obj = JUuid::from_env(env, obj).unwrap(); + let uuid_obj = env.cast_local::(obj).unwrap(); - assert_eq!(uuid_obj.as_uuid().unwrap(), Uuid::from_u128(test.uuid)); + assert_eq!(uuid_obj.as_uuid(env).unwrap(), Uuid::from_u128(test.uuid)); } - }); + Ok(()) + }) + .unwrap(); } } diff --git a/src/droidplug/mod.rs b/src/droidplug/mod.rs index eed9db6a..1ffb3456 100644 --- a/src/droidplug/mod.rs +++ b/src/droidplug/mod.rs @@ -2,7 +2,7 @@ pub mod adapter; pub mod manager; pub mod peripheral; -use ::jni::JNIEnv; +use ::jni::Env; use once_cell::sync::OnceCell; mod jni; @@ -10,9 +10,9 @@ mod jni_utils; static GLOBAL_ADAPTER: OnceCell = OnceCell::new(); -pub fn init(env: &JNIEnv) -> crate::Result<()> { +pub fn init(env: &mut Env) -> crate::Result<()> { self::jni::init(env)?; - GLOBAL_ADAPTER.get_or_try_init(|| adapter::Adapter::new())?; + GLOBAL_ADAPTER.get_or_try_init(adapter::Adapter::new)?; Ok(()) } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 8554376e..04042913 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -1,6 +1,10 @@ +use super::jni::{ + jvm, + objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, +}; use super::jni_utils::{ arrays::byte_array_to_vec, - exceptions::try_block, + exceptions::throwable_to_string, future::{JFuture, JSendFuture}, stream::JSendStream, task::JPollResult, @@ -16,8 +20,8 @@ use crate::{ use async_trait::async_trait; use futures::stream::Stream; use jni::{ - JNIEnv, - objects::{GlobalRef, JList, JObject}, + Env, jni_sig, jni_str, + objects::{Global, JObject, JString, JThrowable, JValue}, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -25,19 +29,12 @@ use serde::{Deserialize, Serialize}; use serde_cr as serde; use std::{ collections::BTreeSet, - convert::TryFrom, fmt::{self, Debug, Display, Formatter}, pin::Pin, sync::atomic::{AtomicU16, Ordering}, sync::{Arc, Mutex}, }; -use uuid::Uuid; -use super::jni::{ - global_jvm, - objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, -}; -use jni::objects::JClass; #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), @@ -51,107 +48,91 @@ impl Display for PeripheralId { } } -fn get_poll_result<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, - result: JPollResult<'a, 'b>, +fn get_poll_result<'a>( + env: &mut Env<'a>, + result_ref: &Global>, ) -> Result> { - try_block(env, || Ok(Ok(result.get()?))) - .catch( - JClass::from( - super::jni_utils::classcache::get_class( - "io/github/gedgygedgy/rust/future/FutureException", - ) - .unwrap() - .as_obj(), - ), - |ex| { + let result_obj = env.new_local_ref(result_ref)?; + let poll_result = env.cast_local::(result_obj)?; + + match poll_result.get(env) { + Ok(obj) => Ok(obj), + Err(jni::errors::Error::JavaException) => { + let ex = env.exception_occurred().unwrap(); + env.exception_clear(); + + use super::jni::objects::*; + use jni::objects::Reference; + + let future_ex_class = + ::lookup_class( + env, + &Default::default(), + )?; + + if env.is_instance_of(&ex, &*future_ex_class)? { let cause = env - .call_method(ex, "getCause", "()Ljava/lang/Throwable;", &[])? + .call_method( + &ex, + jni_str!("getCause"), + jni_sig!("()Ljava/lang/Throwable;"), + &[], + )? .l()?; - if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NotConnectedException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::NotConnected)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::PermissionDenied)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::UnexpectedCallback)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::UnexpectedCharacteristic)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::NoSuchCharacteristic)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::NoAdapterAvailable)) - } else if env.is_instance_of( - cause, - "java/lang/RuntimeException", - )? { + + macro_rules! check_exception { + ($type:ty, $env:expr, $cause:expr) => { + $env.is_instance_of( + $cause, + &*<$type as Reference>::lookup_class($env, &Default::default())?, + )? + }; + } + + if check_exception!(JNotConnectedException, env, &cause) { + Err(Error::NotConnected) + } else if check_exception!(JPermissionDeniedException, env, &cause) { + Err(Error::PermissionDenied) + } else if check_exception!(JUnexpectedCallbackException, env, &cause) { + Err(Error::UnexpectedCallback) + } else if check_exception!(JUnexpectedCharacteristicException, env, &cause) { + Err(Error::UnexpectedCharacteristic) + } else if check_exception!(JNoSuchCharacteristicException, env, &cause) { + Err(Error::NoSuchCharacteristic) + } else if check_exception!(JNoBluetoothAdapterException, env, &cause) { + Err(Error::NoAdapterAvailable) + } else if env.is_instance_of(&cause, jni_str!("java/lang/RuntimeException"))? { let msg = env - .call_method(cause, "getMessage", "()Ljava/lang/String;", &[]) - .unwrap() - .l() - .unwrap(); - let msgstr:String = env.get_string(msg.into()).unwrap().into(); - Ok(Err(Error::RuntimeError(msgstr))) + .call_method( + &cause, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + let jstr = env.cast_local::(msg)?; + let msgstr = String::from(jstr.mutf8_chars(env)?); + Err(Error::RuntimeError(msgstr)) } else { - env.throw(ex)?; - Err(jni::errors::Error::JavaException) + let cause = if cause.is_null() { + None + } else { + Some(env.cast_local::(cause)?) + }; + let desc = if let Some(cause) = &cause { + throwable_to_string(env, cause)? + } else { + throwable_to_string(env, &ex)? + }; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) } - }, - ) - .result()? + } else { + let desc = throwable_to_string(env, &ex)?; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) + } + } + Err(e) => Err(e.into()), + } } #[derive(Debug)] @@ -159,28 +140,27 @@ struct PeripheralShared { services: BTreeSet, characteristics: BTreeSet, properties: Option, - mtu: AtomicU16, } #[derive(Clone)] pub struct Peripheral { addr: BDAddr, - internal: GlobalRef, + internal: Arc>>, shared: Arc>, mtu: Arc, } impl Peripheral { - pub(crate) fn new(env: &JNIEnv, adapter: JObject, addr: BDAddr) -> Result { - let obj = JPeripheral::new(env, adapter, addr)?; + pub(crate) fn new<'a>(env: &mut Env<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { + let obj = JPeripheral::create(env, adapter, addr)?; + let internal = Arc::new(env.new_global_ref(&*obj)?); Ok(Self { addr, - internal: env.new_global_ref(obj)?, + internal, shared: Arc::new(Mutex::new(PeripheralShared { services: BTreeSet::new(), characteristics: BTreeSet::new(), properties: None, - mtu: AtomicU16::new(crate::api::DEFAULT_MTU_SIZE), })), mtu: Arc::new(AtomicU16::new(crate::api::DEFAULT_MTU_SIZE)), }) @@ -188,20 +168,18 @@ impl Peripheral { pub(crate) fn report_properties(&self, properties: PeripheralProperties) { let mut guard = self.shared.lock().unwrap(); - guard.properties = Some(properties); } - fn with_obj( + fn with_obj( &self, - f: impl FnOnce(&JNIEnv, JPeripheral) -> std::result::Result, - ) -> std::result::Result - where - E: From<::jni::errors::Error>, - { - let env = global_jvm().get_env()?; - let obj = JPeripheral::from_env(&env, self.internal.as_obj())?; - f(&env, obj) + f: impl for<'env> FnOnce(&mut Env<'env>, &JPeripheral<'env>) -> Result, + ) -> Result { + jvm()?.attach_current_thread(|env| { + let local_obj = env.new_local_ref(self.internal.as_obj())?; + let obj = env.cast_local::(local_obj)?; + f(env, &obj) + }) } async fn set_characteristic_notification( @@ -211,13 +189,11 @@ impl Peripheral { ) -> Result<()> { let future = self.with_obj(|env, obj| { let uuid_obj = JUuid::new(env, characteristic.uuid)?; - JSendFuture::try_from(obj.set_characteristic_notification(uuid_obj, enable)?) + let future = obj.set_characteristic_notification(env, &uuid_obj, enable)?; + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } } @@ -229,7 +205,6 @@ impl Debug for Peripheral { #[async_trait] impl api::Peripheral for Peripheral { - /// Returns the unique identifier of the peripheral. fn id(&self) -> PeripheralId { PeripheralId(self.addr) } @@ -244,28 +219,30 @@ impl api::Peripheral for Peripheral { async fn properties(&self) -> Result> { let guard = self.shared.lock().map_err(Into::::into)?; - Ok((&guard.properties).clone()) + Ok(guard.properties.clone()) } fn characteristics(&self) -> BTreeSet { let guard = self.shared.lock().unwrap(); - (&guard.characteristics).clone() + guard.characteristics.clone() } async fn is_connected(&self) -> Result { - self.with_obj(|_env, obj| Ok(obj.is_connected()?)) + self.with_obj(|env, obj| Ok(obj.is_connected(env)?)) } async fn connect(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.connect()?))?; - let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - })?; + { + let future = self.with_obj(|env, obj| { + let future = obj.connect(env)?; + Ok(JSendFuture::new(env, &future)?) + })?; + let result_ref = future.await?; + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {}))?; + } // Query the system-cached device name and update local_name - self.with_obj(|_env, obj| -> std::result::Result<(), Error> { - if let Ok(Some(name)) = obj.get_device_name() { + self.with_obj(|env, obj| -> std::result::Result<(), Error> { + if let Ok(Some(name)) = obj.get_device_name(env) { let mut guard = self.shared.lock().map_err(Into::::into)?; if let Some(ref mut props) = guard.properties { props.local_name = Some(name); @@ -274,68 +251,81 @@ impl api::Peripheral for Peripheral { Ok(()) })?; // Auto-negotiate maximum MTU (517) after connection - let mtu_future = self.with_obj(|env, obj| { - JSendFuture::try_from(JFuture::from_env(env, obj.request_mtu(517)?)?) - })?; - let mtu_result_ref = mtu_future.await?; - self.with_obj(|env, _obj| -> Result<()> { - let mtu_result = JPollResult::from_env(env, mtu_result_ref.as_obj())?; - let mtu_obj = get_poll_result(env, mtu_result)?; - let mtu_val = env.call_method(mtu_obj, "intValue", "()I", &[])?.i()?; - self.mtu.store(mtu_val as u16, Ordering::Relaxed); - Ok(()) - })?; + { + let mtu_future = self.with_obj(|env, obj| { + let mtu_obj = obj.request_mtu(env, 517)?; + let mtu_future = env.cast_local::(mtu_obj)?; + Ok(JSendFuture::new(env, &mtu_future)?) + })?; + let mtu_result_ref = mtu_future.await?; + self.with_obj(|env, _obj| -> Result<()> { + let mtu_obj = get_poll_result(env, &mtu_result_ref)?; + let mtu_val = env + .call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])? + .i()?; + self.mtu.store(mtu_val as u16, Ordering::Relaxed); + Ok(()) + })?; + } Ok(()) } async fn disconnect(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.disconnect()?))?; + let future = self.with_obj(|env, obj| { + let future = obj.disconnect(env)?; + Ok(JSendFuture::new(env, &future)?) + })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } - /// The set of services we've discovered for this device. This will be empty until - /// `discover_services` is called. fn services(&self) -> BTreeSet { let guard = self.shared.lock().unwrap(); - (&guard.services).clone() + guard.services.clone() } async fn discover_services(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.discover_services()?))?; + let future = self.with_obj(|env, obj| { + let future = obj.discover_services(env)?; + Ok(JSendFuture::new(env, &future)?) + })?; let result_ref = future.await?; self.with_obj(|env, _obj| { use std::iter::FromIterator; - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let obj = get_poll_result(env, result)?; - let list = JList::from_env(env, obj)?; + let obj = get_poll_result(env, &result_ref)?; + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; let mut peripheral_services = Vec::new(); let mut peripheral_characteristics = Vec::new(); - for service in list.iter()? { - let service = JBluetoothGattService::from_env(env, service)?; + for i in 0..size { + let svc_obj = env + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[JValue::from(i)], + )? + .l()?; + let service = env.cast_local::(svc_obj)?; let mut characteristics = BTreeSet::::new(); - for characteristic in service.get_characteristics()? { + for characteristic in service.get_characteristics(env)? { let mut descriptors = BTreeSet::new(); - for descriptor in characteristic.get_descriptors()? { + for descriptor in characteristic.get_descriptors(env)? { descriptors.insert(Descriptor { - uuid: descriptor.get_uuid()?, - service_uuid: service.get_uuid()?, - characteristic_uuid: characteristic.get_uuid()?, + uuid: descriptor.get_uuid(env)?, + service_uuid: service.get_uuid(env)?, + characteristic_uuid: characteristic.get_uuid(env)?, }); } let char = Characteristic { - service_uuid: service.get_uuid()?, - uuid: characteristic.get_uuid()?, - properties: characteristic.get_properties()?, + service_uuid: service.get_uuid(env)?, + uuid: characteristic.get_uuid(env)?, + properties: characteristic.get_properties(env)?, descriptors: descriptors.clone(), }; - // Only consider the first characteristic of each UUID - // This "should" be unique, but of course it's not enforced if characteristics .iter() .filter(|c| c.service_uuid == char.service_uuid && c.uuid == char.uuid) @@ -347,7 +337,7 @@ impl api::Peripheral for Peripheral { } } peripheral_services.push(Service { - uuid: service.get_uuid()?, + uuid: service.get_uuid(env)?, primary: service.is_primary()?, characteristics, }) @@ -372,25 +362,25 @@ impl api::Peripheral for Peripheral { WriteType::WithResponse => 2, WriteType::WithoutResponse => 1, }; - JSendFuture::try_from(obj.write(uuid, data_obj.into(), write_type)?) + let future = obj.write(env, &uuid, &data_obj.into(), write_type)?; + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } async fn read(&self, characteristic: &Characteristic) -> Result> { let future = self.with_obj(|env, obj| { let uuid = JUuid::new(env, characteristic.uuid)?; - JSendFuture::try_from(obj.read(uuid)?) + let future = obj.read(env, &uuid)?; + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_inner())?) + let bytes_obj = get_poll_result(env, &result_ref)?; + let bytes_arr = + unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; + Ok(byte_array_to_vec(env, &bytes_arr)?) }) } @@ -407,33 +397,42 @@ impl api::Peripheral for Peripheral { async fn notifications(&self) -> Result + Send>>> { use futures::stream::StreamExt; let shared = self.shared.clone(); - let stream = self.with_obj(|_env, obj| JSendStream::try_from(obj.get_notifications()?))?; + let stream = self.with_obj(|env, obj| { + let stream = obj.get_notifications(env)?; + Ok(JSendStream::new(env, &stream)?) + })?; let stream = stream .map(move |item| match item { Ok(item) => { - let env = global_jvm().get_env()?; - let item = item.as_obj(); - let characteristic = JBluetoothGattCharacteristic::from_env(&env, item)?; - let uuid = characteristic.get_uuid()?; - let value = characteristic.get_value()?; - let service_uuid = shared - .lock() - .ok() - .and_then(|guard| { - guard - .services - .iter() - .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) - .map(|s| s.uuid) + let vm = jvm()?; + let result: crate::Result<_> = vm + .attach_current_thread(|env| -> jni::errors::Result<_> { + let local_obj = env.new_local_ref(item.as_obj())?; + let characteristic = + env.cast_local::(local_obj)?; + let uuid = characteristic.get_uuid(env)?; + let value = characteristic.get_value(env)?; + let service_uuid = shared + .lock() + .ok() + .and_then(|guard| { + guard + .services + .iter() + .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) + .map(|s| s.uuid) + }) + .unwrap_or_default(); + Ok(ValueNotification { + uuid, + service_uuid, + value, + }) }) - .unwrap_or_default(); - Ok(ValueNotification { - uuid, - service_uuid, - value, - }) + .map_err(Into::into); + result } - Err(err) => Err(err), + Err(err) => Err(err.into()), }) .filter_map(|item| async { item.ok() }); Ok(Box::pin(stream)) @@ -441,13 +440,16 @@ impl api::Peripheral for Peripheral { async fn read_rssi(&self) -> Result { let future = self.with_obj(|env, obj| { - JSendFuture::try_from(JFuture::from_env(env, obj.read_remote_rssi()?)?) + let rssi_obj = obj.read_remote_rssi(env)?; + let rssi_future = env.cast_local::(rssi_obj)?; + Ok(JSendFuture::new(env, &rssi_future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let rssi_obj = get_poll_result(env, result)?; - let rssi_val = env.call_method(rssi_obj, "intValue", "()I", &[])?.i()?; + let rssi_obj = get_poll_result(env, &result_ref)?; + let rssi_val = env + .call_method(&rssi_obj, jni_str!("intValue"), jni_sig!("()I"), &[])? + .i()?; Ok(rssi_val as i16) }) } @@ -457,46 +459,45 @@ impl api::Peripheral for Peripheral { let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; let uuid = JUuid::new(env, descriptor.uuid)?; let data_obj = super::jni_utils::arrays::slice_to_byte_array(env, data)?; - JSendFuture::try_from(obj.write_descriptor(characteristic, uuid, data_obj.into())?) + let future = obj.write_descriptor(env, &characteristic, &uuid, &data_obj.into())?; + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } async fn read_descriptor(&self, descriptor: &Descriptor) -> Result> { let future = self.with_obj(|env, obj| { let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; let uuid = JUuid::new(env, descriptor.uuid)?; - JSendFuture::try_from(obj.read_descriptor(characteristic, uuid)?) + let future = obj.read_descriptor(env, &characteristic, &uuid)?; + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_inner())?) + let bytes_obj = get_poll_result(env, &result_ref)?; + let bytes_arr = + unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; + Ok(byte_array_to_vec(env, &bytes_arr)?) }) } async fn connection_parameters(&self) -> Result> { - self.with_obj(|_env, obj| { - Ok(obj - .get_connection_parameters() - .map_err(|e| Error::Other(format!("{:?}", e).into()))?) + self.with_obj(|env, obj| { + obj.get_connection_parameters(env) + .map_err(|e| Error::Other(format!("{:?}", e).into())) }) } async fn request_connection_parameters(&self, preset: ConnectionParameterPreset) -> Result<()> { let priority = match preset { - ConnectionParameterPreset::Balanced => 0, // CONNECTION_PRIORITY_BALANCED - ConnectionParameterPreset::ThroughputOptimized => 1, // CONNECTION_PRIORITY_HIGH - ConnectionParameterPreset::PowerOptimized => 2, // CONNECTION_PRIORITY_LOW_POWER + ConnectionParameterPreset::Balanced => 0, + ConnectionParameterPreset::ThroughputOptimized => 1, + ConnectionParameterPreset::PowerOptimized => 2, }; - self.with_obj(|_env, obj| { + self.with_obj(|env, obj| { let success = obj - .request_connection_priority(priority) + .request_connection_priority(env, priority) .map_err(|e| Error::Other(format!("{:?}", e).into()))?; if success { Ok(()) diff --git a/src/lib.rs b/src/lib.rs index e2181590..368b4690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,8 @@ use crate::api::ParseBDAddrError; use std::result; use std::time::Duration; +#[cfg(any(target_os = "android", target_os = "windows", test))] +mod advertisement; pub mod api; #[cfg(target_os = "linux")] mod bluez; @@ -99,6 +101,7 @@ mod corebluetooth; #[cfg(target_os = "android")] mod droidplug; #[cfg(all(not(target_os = "android"), feature = "jni-host-tests"))] +#[allow(dead_code)] mod droidplug { mod jni_utils; } diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index c4cba39d..16d0665c 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -14,17 +14,27 @@ use super::{ble::watcher::BLEWatcher, peripheral::Peripheral, peripheral::PeripheralId}; use crate::{ Error, Result, - api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}, + api::{ + self, BDAddr, Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter, + }, common::adapter_manager::AdapterManager, }; use async_trait::async_trait; use futures::stream::Stream; -use std::convert::TryInto; +use std::convert::TryFrom; use std::fmt::{self, Debug, Formatter}; +use std::future::IntoFuture; use std::pin::Pin; use std::sync::{Arc, Mutex}; use windows::{ - Devices::Radios::{Radio, RadioState}, + Devices::{ + Bluetooth::{ + BluetoothAdapter, BluetoothCacheMode, BluetoothLEDevice, + GenericAttributeProfile::GattCommunicationStatus, + }, + Enumeration::DeviceInformation, + Radios::{Radio, RadioState}, + }, Foundation::TypedEventHandler, }; @@ -34,9 +44,18 @@ pub struct Adapter { watcher: Arc>, manager: Arc>, radio: Radio, + bluetooth_adapter: BluetoothAdapter, } // https://github.com/microsoft/windows-rs/blob/master/crates/libs/windows/src/Windows/Devices/Radios/mod.rs +fn winrt_error(error: E) -> Error { + Error::Other(format!("{error:?}").into()) +} + +fn checked_address(value: u64) -> Result { + BDAddr::try_from(value).map_err(Error::from) +} + fn get_central_state(radio: &Radio) -> CentralState { let state = radio.State().unwrap_or(RadioState::Unknown); match state { @@ -47,7 +66,7 @@ fn get_central_state(radio: &Radio) -> CentralState { } impl Adapter { - pub(crate) fn new(radio: Radio) -> Result { + pub(crate) fn new(bluetooth_adapter: BluetoothAdapter, radio: Radio) -> Result { let watcher = Arc::new(Mutex::new(BLEWatcher::new()?)); let manager = Arc::new(AdapterManager::default()); @@ -55,7 +74,7 @@ impl Adapter { let manager_clone = manager.clone(); let handler = TypedEventHandler::new(move |_sender, _args| { let state = get_central_state(&radio_clone); - manager_clone.emit(CentralEvent::StateUpdate(state.into())); + manager_clone.emit(CentralEvent::StateUpdate(state)); Ok(()) }); if let Err(err) = radio.StateChanged(&handler) { @@ -66,10 +85,54 @@ impl Adapter { watcher, manager, radio, + bluetooth_adapter, }) } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_address_rejects_values_outside_six_bytes() { + assert!(checked_address(0x11_22_33_44_55_66_77).is_err()); + } + + #[test] + fn checked_address_preserves_windows_address_order() { + assert_eq!( + checked_address(0x11_22_33_44_55_66).unwrap().into_inner(), + [0x11, 0x22, 0x33, 0x44, 0x55, 0x66] + ); + } + + #[test] + fn retrieve_selector_union_matches_identifier_or_service() { + let id = PeripheralId::from(BDAddr::from([1, 2, 3, 4, 5, 6])); + let options = RetrievePeripheralsOptions { + identifiers: Some(vec![id.clone()]), + services: Some(vec![uuid::Uuid::nil()]), + }; + assert!(api::matches_retrieval_selectors(&id, &[], &options)); + assert!(api::matches_retrieval_selectors( + &PeripheralId::from(BDAddr::from([6, 5, 4, 3, 2, 1])), + &[uuid::Uuid::nil()], + &options + )); + } + + #[test] + fn retrieve_selector_empty_values_match_nothing() { + let id = PeripheralId::from(BDAddr::from([1, 2, 3, 4, 5, 6])); + let options = RetrievePeripheralsOptions { + identifiers: Some(vec![]), + services: None, + }; + assert!(!api::matches_retrieval_selectors(&id, &[], &options)); + } +} + impl Debug for Adapter { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Adapter") @@ -87,13 +150,18 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - let watcher = self.watcher.lock().map_err(Into::::into)?; + let mut watcher = self.watcher.lock().map_err(Into::::into)?; let manager = self.manager.clone(); watcher.start( filter, Box::new(move |args| { let bluetooth_address = args.BluetoothAddress()?; - let address: BDAddr = bluetooth_address.try_into().unwrap(); + let address = checked_address(bluetooth_address).map_err(|error| { + windows::core::Error::new( + windows::core::HRESULT::from_win32(87), + error.to_string(), + ) + })?; if let Some(mut entry) = manager.peripheral_mut(&address.into()) { entry.value_mut().update_properties(args); manager.emit(CentralEvent::DeviceUpdated(address.into())); @@ -109,7 +177,7 @@ impl Central for Adapter { } async fn stop_scan(&self) -> Result<()> { - let watcher = self.watcher.lock().map_err(Into::::into)?; + let mut watcher = self.watcher.lock().map_err(Into::::into)?; watcher.stop()?; Ok(()) } @@ -118,14 +186,128 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + /// Retrieves connected BLE devices from the Windows device enumeration service. + /// + /// WinRT's connected-device selector is system-wide and cannot be restricted to + /// this `Radio`; callers must treat results as belonging to the Windows BLE + /// subsystem rather than to one physical adapter when multiple radios exist. + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + // Identifier-only retrieval must not use the connected-device selector: it is + // intentionally independent of enumeration, and preserves the requested ID order. + if options.identifiers.is_some() && options.services.is_none() { + let mut result = Vec::new(); + for requested_id in options.identifiers.as_deref().unwrap_or_default() { + let async_operation = match BluetoothLEDevice::FromBluetoothAddressAsync( + requested_id.address().into(), + ) { + Ok(async_operation) => async_operation, + // Unknown cached IDs are omitted, not errors. + Err(_) => continue, + }; + let device = match async_operation.into_future().await { + Ok(device) => device, + // Disconnected cached IDs are omitted, not errors. + Err(_) => continue, + }; + if device.ConnectionStatus().map_err(winrt_error)? + != windows::Devices::Bluetooth::BluetoothConnectionStatus::Connected + { + continue; + } + let address = checked_address(device.BluetoothAddress().map_err(winrt_error)?)?; + let peripheral = self + .manager + .peripheral(&PeripheralId::from(address)) + .unwrap_or_else(|| { + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), address); + self.manager.add_peripheral(peripheral.clone()); + peripheral + }); + result.push(peripheral); + } + return Ok(api::merge_retrieved_peripherals(result, |peripheral| { + crate::api::Peripheral::id(peripheral) + })); + } + + // Service and combined retrieval use WinRT's connected-device enumeration. + let selector = BluetoothLEDevice::GetDeviceSelectorFromConnectionStatus( + windows::Devices::Bluetooth::BluetoothConnectionStatus::Connected, + ) + .map_err(winrt_error)?; + let devices = DeviceInformation::FindAllAsyncAqsFilter(&selector) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)? + .into_iter() + .collect::>(); + let mut result = Vec::new(); + + for info in devices { + let id = info.Id().map_err(winrt_error)?; + let device = BluetoothLEDevice::FromIdAsync(&id) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)?; + let address = checked_address(device.BluetoothAddress().map_err(winrt_error)?)?; + let candidate_id = PeripheralId::from(address); + + let service_result = device + .GetGattServicesWithCacheModeAsync(BluetoothCacheMode::Cached) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)?; + let service_uuids = if service_result.Status().map_err(winrt_error)? + == GattCommunicationStatus::Success + { + service_result + .Services() + .map_err(winrt_error)? + .into_iter() + .map(|service| { + service + .Uuid() + .map(|uuid| crate::winrtble::utils::to_uuid(&uuid)) + }) + .collect::>>() + .map_err(winrt_error)? + } else { + Vec::new() + }; + if !api::matches_retrieval_selectors(&candidate_id, &service_uuids, &options) { + continue; + } + let peripheral = self.manager.peripheral(&candidate_id).unwrap_or_else(|| { + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), address); + self.manager.add_peripheral(peripheral.clone()); + peripheral + }); + result.push(peripheral); + } + Ok(api::merge_retrieved_peripherals(result, |peripheral| { + crate::api::Peripheral::id(peripheral) + })) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } - async fn add_peripheral(&self, _address: &PeripheralId) -> Result { - Err(Error::NotSupported( - "Can't add a Peripheral from a BDAddr".to_string(), - )) + async fn add_peripheral(&self, id: &PeripheralId) -> Result { + if let Some(peripheral) = self.manager.peripheral(id) { + return Ok(peripheral); + } + // Create a peripheral straight from its address so a device the OS already knows (bonded or + // connected to another central) can be reached without waiting for an advertisement. + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), id.clone().into()); + self.manager.add_peripheral(peripheral.clone()); + Ok(peripheral) } async fn clear_peripherals(&self) -> Result<()> { @@ -138,6 +320,17 @@ impl Central for Adapter { Ok("WinRT".to_string()) } + async fn adapter_address(&self) -> Result> { + let bluetooth_address = self.bluetooth_adapter.BluetoothAddress().map_err(|error| { + Error::Other(format!("Could not get Bluetooth adapter address: {error:?}").into()) + })?; + if bluetooth_address == 0 { + return Ok(None); + } + let address: BDAddr = bluetooth_address.try_into()?; + Ok(Some(address)) + } + async fn adapter_state(&self) -> Result { Ok(get_central_state(&self.radio)) } diff --git a/src/winrtble/ble/characteristic.rs b/src/winrtble/ble/characteristic.rs index 0792ad3d..eea70324 100644 --- a/src/winrtble/ble/characteristic.rs +++ b/src/winrtble/ble/characteristic.rs @@ -100,8 +100,28 @@ impl BLECharacteristic { } } + fn remove_notify_handler(&mut self) -> Result<()> { + if let Some(token) = self.notify_token { + // Only relinquish ownership after WinRT confirms removal. This keeps + // the token available for a later retry when removal fails. + self.characteristic.RemoveValueChanged(token)?; + self.notify_token = None; + } + Ok(()) + } + pub async fn subscribe(&mut self, on_value_changed: NotifiyEventHandler) -> Result<()> { - { + // Validate before changing the existing subscription state. + let config = to_descriptor_value(self.characteristic.CharacteristicProperties()?); + if config == GattClientCharacteristicConfigurationDescriptorValue::None { + return Err(Error::NotSupported("Can not subscribe to attribute".into())); + } + + // A replacement is allowed, but never leave two handlers installed. If + // removal fails, retain the old token and reject the replacement. + self.remove_notify_handler()?; + + let token = { let value_handler = TypedEventHandler::new( move |_: Ref, args: Ref| { if let Ok(args) = args.ok() { @@ -116,23 +136,32 @@ impl BLECharacteristic { Ok(()) }, ); - let token = self.characteristic.ValueChanged(&value_handler)?; - self.notify_token = Some(token); - } - let config = to_descriptor_value(self.characteristic.CharacteristicProperties()?); - if config == GattClientCharacteristicConfigurationDescriptorValue::None { - return Err(Error::NotSupported("Can not subscribe to attribute".into())); - } + self.characteristic.ValueChanged(&value_handler)? + }; + self.notify_token = Some(token); - let status = self + let status = match self .characteristic - .WriteClientCharacteristicConfigurationDescriptorAsync(config)? - .into_future() - .await?; + .WriteClientCharacteristicConfigurationDescriptorAsync(config) + { + Ok(operation) => operation.into_future().await, + Err(err) => { + let _ = self.remove_notify_handler(); + return Err(err.into()); + } + }; + let status = match status { + Ok(status) => status, + Err(err) => { + let _ = self.remove_notify_handler(); + return Err(err.into()); + } + }; trace!("subscribe {:?}", status); if status == GattCommunicationStatus::Success { Ok(()) } else { + let _ = self.remove_notify_handler(); Err(Error::Other( format!("Windows UWP threw error on subscribe: {:?}", status).into(), )) @@ -140,10 +169,8 @@ impl BLECharacteristic { } pub async fn unsubscribe(&mut self) -> Result<()> { - if let Some(token) = &self.notify_token { - self.characteristic.RemoveValueChanged(*token)?; - } - self.notify_token = None; + // Disable the CCCD first. If that fails, retain the token and handler so + // ownership is still available for a later cleanup retry. let config = GattClientCharacteristicConfigurationDescriptorValue::None; let status = self .characteristic @@ -151,13 +178,14 @@ impl BLECharacteristic { .into_future() .await?; trace!("unsubscribe {:?}", status); - if status == GattCommunicationStatus::Success { - Ok(()) - } else { - Err(Error::Other( + if status != GattCommunicationStatus::Success { + return Err(Error::Other( format!("Windows UWP threw error on unsubscribe: {:?}", status).into(), - )) + )); } + + // Keep the token if removal fails; the next unsubscribe (or Drop) can retry. + self.remove_notify_handler() } pub fn uuid(&self) -> Uuid { diff --git a/src/winrtble/ble/device.rs b/src/winrtble/ble/device.rs index 64bfef37..195f7fad 100644 --- a/src/winrtble/ble/device.rs +++ b/src/winrtble/ble/device.rs @@ -60,10 +60,8 @@ impl BLEDevice { let connection_status_handler = TypedEventHandler::::new(move |sender, _| { if let Some(sender) = sender.as_ref() { - let is_connected = sender - .ConnectionStatus() - .ok() - .map_or(false, |v| v == BluetoothConnectionStatus::Connected); + let is_connected = sender.ConnectionStatus().ok() + == Some(BluetoothConnectionStatus::Connected); connection_status_changed(is_connected); trace!("state {:?}", sender.ConnectionStatus()); } @@ -211,7 +209,7 @@ impl BLEDevice { let params = self.device.GetConnectionParameters().map_err(winrt_error)?; // ConnectionInterval is in units of 1.25ms, convert to microseconds let interval_us = (params.ConnectionInterval().map_err(winrt_error)? as u32) * 1250; - let latency = params.ConnectionLatency().map_err(winrt_error)? as u16; + let latency = params.ConnectionLatency().map_err(winrt_error)?; // LinkTimeout is in units of 10ms, convert to microseconds let supervision_timeout_us = (params.LinkTimeout().map_err(winrt_error)? as u32) * 10_000; Ok(crate::api::ConnectionParameters { diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 59881ccb..88774a5f 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -12,14 +12,18 @@ // Copyright (c) 2014 The Rust Project Developers use crate::{Error, Result, api::ScanFilter, winrtble::utils}; +use std::{collections::HashSet, sync::Mutex}; use windows::{Devices::Bluetooth::Advertisement::*, Foundation::TypedEventHandler, core::Ref}; +const MATCH_CACHE_CAPACITY: usize = 1024; + pub type AdvertisementEventHandler = Box windows::core::Result<()> + Send>; #[derive(Debug)] pub struct BLEWatcher { watcher: BluetoothLEAdvertisementWatcher, + received_token: Option, } impl From for Error { @@ -28,14 +32,39 @@ impl From for Error { } } +#[derive(Default)] +struct MatchCache { + addresses: HashSet, +} + +impl MatchCache { + fn record(&mut self, address: u64) { + if self.addresses.len() < MATCH_CACHE_CAPACITY || self.addresses.contains(&address) { + self.addresses.insert(address); + } + } + + fn contains(&self, address: u64) -> bool { + self.addresses.contains(&address) + } +} + impl BLEWatcher { pub fn new() -> Result { let ad = BluetoothLEAdvertisementFilter::new()?; let watcher = BluetoothLEAdvertisementWatcher::Create(&ad)?; - Ok(BLEWatcher { watcher }) + Ok(BLEWatcher { + watcher, + received_token: None, + }) } - pub fn start(&self, filter: ScanFilter, on_received: AdvertisementEventHandler) -> Result<()> { + pub fn start( + &mut self, + filter: ScanFilter, + on_received: AdvertisementEventHandler, + ) -> Result<()> { + self.remove_received_handler()?; let ScanFilter { services } = filter; // Clear any OS-level service UUID filter from a previous scan. @@ -48,9 +77,15 @@ impl BLEWatcher { self.watcher .SetScanningMode(BluetoothLEScanningMode::Active)?; let _ = self.watcher.SetAllowExtendedAdvertisements(true); + // Also receive on the Coded (long-range) PHY where the adapter and + // OS support it. Only takes effect alongside extended advertisements + // (above); the error is ignored the same way, so systems without + // Coded PHY support behave exactly as before. + let _ = self.watcher.SetUseCodedPhy(true); // Pre-convert the filter UUIDs once so the handler closure is cheap. let filter_guids: Vec = services.iter().map(utils::to_guid).collect(); + let matching_devices = Mutex::new(MatchCache::default()); let handler: TypedEventHandler< BluetoothLEAdvertisementWatcher, @@ -60,18 +95,30 @@ impl BLEWatcher { if let Ok(args) = args.ok() { // Software service-UUID filter. if !filter_guids.is_empty() { - if let Ok(ad) = args.Advertisement() { - if let Ok(ad_uuids) = ad.ServiceUuids() { - let count = ad_uuids.Size().unwrap_or(0); + let address = args.BluetoothAddress().unwrap_or(0); + let mut is_match = false; + + if let Ok(ad) = args.Advertisement() + && let Ok(ad_uuids) = ad.ServiceUuids() + { + let count = ad_uuids.Size().unwrap_or(0); + if count > 0 { let advertised: Vec = (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); - let all_present = - filter_guids.iter().all(|g| advertised.contains(g)); - if !all_present { - return Ok(()); - } + is_match = filter_guids.iter().any(|g| advertised.contains(g)); } } + + let mut cache = matching_devices.lock().unwrap(); + if is_match { + cache.record(address); + } else if !matches!( + args.AdvertisementType(), + Ok(BluetoothLEAdvertisementType::ScanResponse) + ) || !cache.contains(address) + { + return Ok(()); + } } on_received(args)?; } @@ -79,13 +126,38 @@ impl BLEWatcher { }, ); - self.watcher.Received(&handler)?; + self.received_token = Some(self.watcher.Received(&handler)?); self.watcher.Start()?; Ok(()) } - pub fn stop(&self) -> Result<()> { + pub fn stop(&mut self) -> Result<()> { self.watcher.Stop()?; + self.remove_received_handler() + } + + fn remove_received_handler(&mut self) -> Result<()> { + if let Some(token) = self.received_token.take() { + self.watcher.RemoveReceived(token)?; + } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{MATCH_CACHE_CAPACITY, MatchCache}; + + #[test] + fn match_cache_is_bounded() { + let mut cache = MatchCache::default(); + for address in 0..MATCH_CACHE_CAPACITY as u64 { + cache.record(address); + } + cache.record(MATCH_CACHE_CAPACITY as u64); + + assert_eq!(cache.addresses.len(), MATCH_CACHE_CAPACITY); + assert!(cache.contains(0)); + assert!(!cache.contains(MATCH_CACHE_CAPACITY as u64)); + } +} diff --git a/src/winrtble/manager.rs b/src/winrtble/manager.rs index b1230208..e9417707 100644 --- a/src/winrtble/manager.rs +++ b/src/winrtble/manager.rs @@ -15,7 +15,7 @@ use super::adapter::Adapter; use crate::{Result, api}; use async_trait::async_trait; use std::future::IntoFuture; -use windows::Devices::Radios::{Radio, RadioKind}; +use windows::Devices::{Bluetooth::BluetoothAdapter, Enumeration::DeviceInformation}; /// Implementation of [api::Manager](crate::api::Manager). #[derive(Clone, Debug)] @@ -32,11 +32,21 @@ impl api::Manager for Manager { type Adapter = Adapter; async fn adapters(&self) -> Result> { - let radios = Radio::GetRadiosAsync()?.into_future().await?; - radios + let selector = BluetoothAdapter::GetDeviceSelector()?; + let devices = DeviceInformation::FindAllAsyncAqsFilter(&selector)? + .into_future() + .await? .into_iter() - .filter(|radio| radio.Kind() == Ok(RadioKind::Bluetooth)) - .map(|radio| Adapter::new(radio)) - .collect() + .collect::>(); + let mut adapters = Vec::new(); + for device in devices { + let device_id = device.Id()?; + let bluetooth_adapter = BluetoothAdapter::FromIdAsync(&device_id)? + .into_future() + .await?; + let radio = bluetooth_adapter.GetRadioAsync()?.into_future().await?; + adapters.push(Adapter::new(bluetooth_adapter, radio)?); + } + Ok(adapters) } } diff --git a/src/winrtble/mod.rs b/src/winrtble/mod.rs index 2d9920a9..39ebf434 100644 --- a/src/winrtble/mod.rs +++ b/src/winrtble/mod.rs @@ -22,4 +22,6 @@ mod advertisement_data_type { pub const SERVICE_DATA_16_BIT_UUID: u8 = 0x16; pub const SERVICE_DATA_32_BIT_UUID: u8 = 0x20; pub const SERVICE_DATA_128_BIT_UUID: u8 = 0x21; + pub const SHORT_LOCAL_NAME: u8 = 0x08; + pub const COMPLETE_LOCAL_NAME: u8 = 0x09; } diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 7924d678..26602e9c 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -57,6 +57,12 @@ use windows::core::GUID; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct PeripheralId(BDAddr); +impl PeripheralId { + pub(crate) fn address(&self) -> BDAddr { + self.0 + } +} + impl Display for PeripheralId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { Display::fmt(&self.0, f) @@ -81,7 +87,9 @@ struct Shared { // Mutable, advertised, state... address_type: RwLock>, local_name: RwLock>, + has_complete_local_name: AtomicBool, advertisement_name: RwLock>, + appearance: RwLock>, last_tx_power_level: RwLock>, // XXX: would be nice to avoid lock here! last_rssi: RwLock>, // XXX: would be nice to avoid lock here! latest_manufacturer_data: RwLock>>, @@ -90,6 +98,23 @@ struct Shared { class: RwLock>, } +struct AdvertisedName { + value: String, + is_complete: bool, +} + +fn parse_advertised_name(data: &[u8], is_complete: bool) -> Option { + let value = std::str::from_utf8(data) + .ok()? + .trim_end_matches('\0') + .to_string(); + (!value.is_empty()).then_some(AdvertisedName { value, is_complete }) +} + +fn should_accept_name(has_complete_name: bool, new_name_is_complete: bool) -> bool { + !has_complete_name || new_name_is_complete +} + impl Peripheral { pub(crate) fn new(adapter: Weak>, address: BDAddr) -> Self { let (broadcast_sender, _) = broadcast::channel(16); @@ -104,7 +129,9 @@ impl Peripheral { notifications_channel: broadcast_sender, address_type: RwLock::new(None), local_name: RwLock::new(None), + has_complete_local_name: AtomicBool::new(false), advertisement_name: RwLock::new(None), + appearance: RwLock::new(None), last_tx_power_level: RwLock::new(None), last_rssi: RwLock::new(None), latest_manufacturer_data: RwLock::new(HashMap::new()), @@ -123,6 +150,7 @@ impl Peripheral { address_type: *self.shared.address_type.read().unwrap(), local_name: self.shared.local_name.read().unwrap().clone(), advertisement_name: self.shared.advertisement_name.read().unwrap().clone(), + appearance: *self.shared.appearance.read().unwrap(), tx_power_level: *self.shared.last_tx_power_level.read().unwrap(), rssi: *self.shared.last_rssi.read().unwrap(), manufacturer_data: self.shared.latest_manufacturer_data.read().unwrap().clone(), @@ -143,59 +171,99 @@ impl Peripheral { let advertisement = args.Advertisement().unwrap(); // Advertisements are cumulative: set/replace data only if it's set - if let Ok(name) = advertisement.LocalName() { - if !name.is_empty() { - let name_str = name.to_string(); - let mut adv_name_guard = self.shared.advertisement_name.write().unwrap(); - *adv_name_guard = Some(name_str.clone()); - drop(adv_name_guard); - // Also use as local_name fallback if we don't have one yet - let local_name_guard = self.shared.local_name.read().unwrap(); - if local_name_guard.is_none() { - drop(local_name_guard); - let mut local_name_guard = self.shared.local_name.write().unwrap(); - *local_name_guard = Some(name_str); - } - } + let projected_local_name = advertisement + .LocalName() + .ok() + .map(|name| name.to_string()) + .filter(|name| !name.is_empty()); + if let Some(name) = &projected_local_name { + *self.shared.advertisement_name.write().unwrap() = Some(name.clone()); } - if let Ok(manufacturer_data) = advertisement.ManufacturerData() { - if manufacturer_data.Size().unwrap() > 0 { - let mut manufacturer_data_guard = - self.shared.latest_manufacturer_data.write().unwrap(); - *manufacturer_data_guard = manufacturer_data - .into_iter() - .map(|d| { - let manufacturer_id = d.CompanyId().unwrap(); - let data = utils::to_vec(&d.Data().unwrap()); - - (manufacturer_id, data) - }) - .collect(); - - // Emit event of newly received advertisement - self.emit_event(CentralEvent::ManufacturerDataAdvertisement { - id: self.shared.address.into(), - manufacturer_data: manufacturer_data_guard.clone(), - }); - } + if let Ok(manufacturer_data) = advertisement.ManufacturerData() + && manufacturer_data.Size().unwrap() > 0 + { + let mut manufacturer_data_guard = self.shared.latest_manufacturer_data.write().unwrap(); + *manufacturer_data_guard = manufacturer_data + .into_iter() + .map(|d| { + let manufacturer_id = d.CompanyId().unwrap(); + let data = utils::to_vec(&d.Data().unwrap()); + + (manufacturer_id, data) + }) + .collect(); + + // Emit event of newly received advertisement + self.emit_event(CentralEvent::ManufacturerDataAdvertisement { + id: self.shared.address.into(), + manufacturer_data: manufacturer_data_guard.clone(), + }); } // The Windows Runtime API (as of 19041) does not directly expose Service Data as a friendly API (like Manufacturer Data above) // Instead they provide data sections for access to raw advertising data. That is processed here. if let Ok(data_sections) = advertisement.DataSections() { - // See if we have any advertised service data before taking a lock to update... let mut found_service_data = false; + let mut advertised_name = None; + let mut appearance = None; for section in &data_sections { - match section.DataType().unwrap() { + let Ok(data_type) = section.DataType() else { + continue; + }; + match data_type { advertisement_data_type::SERVICE_DATA_16_BIT_UUID | advertisement_data_type::SERVICE_DATA_32_BIT_UUID | advertisement_data_type::SERVICE_DATA_128_BIT_UUID => { found_service_data = true; - break; + } + advertisement_data_type::COMPLETE_LOCAL_NAME => { + if let Some(name) = + parse_advertised_name(&utils::to_vec(§ion.Data().unwrap()), true) + { + advertised_name = Some(name); + } + } + advertisement_data_type::SHORT_LOCAL_NAME if advertised_name.is_none() => { + advertised_name = + parse_advertised_name(&utils::to_vec(§ion.Data().unwrap()), false); + } + crate::advertisement::APPEARANCE_DATA_TYPE => { + if let Ok(data) = section.Data() + && let Some(parsed) = + crate::advertisement::parse_appearance(&utils::to_vec(&data)) + { + appearance = Some(parsed); + } } _ => {} } } + + let has_complete_name = self.shared.has_complete_local_name.load(Ordering::Relaxed); + let name = advertised_name.or_else(|| { + (!has_complete_name) + .then(|| projected_local_name.clone()) + .flatten() + .map(|value| AdvertisedName { + value, + is_complete: false, + }) + }); + if let Some(name) = name + && should_accept_name(has_complete_name, name.is_complete) + { + *self.shared.local_name.write().unwrap() = Some(name.value); + if name.is_complete { + self.shared + .has_complete_local_name + .store(true, Ordering::Relaxed); + } + } + + if let Some(appearance) = appearance { + *self.shared.appearance.write().unwrap() = Some(appearance); + } + if found_service_data { let mut service_data_guard = self.shared.latest_service_data.write().unwrap(); @@ -314,6 +382,33 @@ impl Peripheral { } } +#[cfg(test)] +mod tests { + use super::{parse_advertised_name, should_accept_name}; + + #[test] + fn advertised_name_removes_only_nul_padding() { + let name = parse_advertised_name(b" Device Name \0\0", true).unwrap(); + + assert_eq!(name.value, " Device Name "); + assert!(name.is_complete); + } + + #[test] + fn advertised_name_rejects_empty_and_invalid_utf8() { + assert!(parse_advertised_name(b"\0\0", false).is_none()); + assert!(parse_advertised_name(&[0xff], true).is_none()); + } + + #[test] + fn complete_name_cannot_be_replaced_by_short_name() { + assert!(!should_accept_name(true, false)); + assert!(should_accept_name(true, true)); + assert!(should_accept_name(false, false)); + assert!(should_accept_name(false, true)); + } +} + impl Display for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let connected = if self.shared.connected.load(Ordering::Relaxed) { @@ -401,10 +496,8 @@ impl ApiPeripheral for Peripheral { shared.connected.store(is_connected, Ordering::Relaxed); } - if !is_connected { - if let Some(adapter) = adapter_clone.upgrade() { - adapter.emit(CentralEvent::DeviceDisconnected(address.into())); - } + if !is_connected && let Some(adapter) = adapter_clone.upgrade() { + adapter.emit(CentralEvent::DeviceDisconnected(address.into())); } } }); @@ -471,14 +564,12 @@ impl ApiPeripheral for Peripheral { HashMap::::new(), |mut map, gatt_characteristic| { let uuid = gatt_characteristic.Uuid().unwrap_or_default(); - if !map.contains_key(&uuid) { - map.insert(uuid, gatt_characteristic); - } + map.entry(uuid).or_insert(gatt_characteristic); map }, ) - .into_iter() - .map(|(_, characteristic)| async { + .into_values() + .map(|characteristic| async { let c = characteristic.clone(); ( characteristic, @@ -671,3 +762,9 @@ impl From for PeripheralId { PeripheralId(address) } } + +impl From for BDAddr { + fn from(id: PeripheralId) -> Self { + id.0 + } +} diff --git a/test-peripheral/bumble/test_peripheral.py b/test-peripheral/bumble/test_peripheral.py index 2fedd33b..2f58d99a 100644 --- a/test-peripheral/bumble/test_peripheral.py +++ b/test-peripheral/bumble/test_peripheral.py @@ -66,6 +66,7 @@ CMD_SET_NOTIFICATION_PAYLOAD = 0x06 DEVICE_NAME = "btleplug-test" +TEST_APPEARANCE = 0x0340 MANUFACTURER_COMPANY_ID = 0xFFFF STATIC_READ_VALUE = bytes([0x01, 0x02, 0x03, 0x04]) NOTIFICATION_INTERVAL = 1.0 # seconds @@ -409,6 +410,7 @@ async def main(): struct.pack(" &'static Runtime { const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(55); /// Run a test function on the global runtime, converting panics to JNI exceptions. -fn run_test(env: &JNIEnv, test_name: &str, f: impl std::future::Future) { +fn run_test(env: &mut Env, test_name: &str, f: impl std::future::Future) { log::info!("[START] {}", test_name); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { runtime().block_on(async { @@ -78,7 +79,8 @@ fn run_test(env: &JNIEnv, test_name: &str, f: impl std::future::Future(); } // ── Test JNI exports ──────────────────────────────────────────────── -// + // Each function follows the JNI naming convention: // Java_com_nonpolynomial_btleplug_test_NativeTests_ macro_rules! jni_test { ($jni_name:ident, $test_fn:path) => { #[unsafe(no_mangle)] - pub extern "system" fn $jni_name(env: JNIEnv, _class: JClass) { - run_test(&env, stringify!($test_fn), $test_fn()); + pub extern "system" fn $jni_name(mut env: EnvUnowned, _class: JClass) { + env.with_env(|env| { + run_test(env, stringify!($test_fn), $test_fn()); + Ok::<_, jni::errors::Error>(()) + }) + .resolve::(); } }; } +jni_test!( + Java_com_nonpolynomial_btleplug_test_NativeTests_testAdapterAddress, + test_cases::test_adapter_address +); + jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testDiscoverPeripheralByName, test_cases::test_discover_peripheral_by_name @@ -135,6 +147,10 @@ jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testAdvertisementServices, test_cases::test_advertisement_services ); +jni_test!( + Java_com_nonpolynomial_btleplug_test_NativeTests_testRetrievePeripheralsNotSupported, + test_cases::test_retrieve_peripherals_not_supported +); jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testConnectAndDisconnect, test_cases::test_connect_and_disconnect @@ -208,8 +224,8 @@ jni_test!( test_cases::test_descriptor_discovery ); jni_test!( - Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterConnection, - test_cases::test_mtu_after_connection + Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterServiceDiscovery, + test_cases::test_mtu_after_service_discovery ); jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testReadRssi, diff --git a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt index ccb5dd7f..87f1eb65 100644 --- a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt +++ b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt @@ -62,6 +62,9 @@ class BleIntegrationTest { } } + // ── Adapter capabilities ──────────────────────────────────────── + @Test fun testAdapterAddress() = NativeTests.testAdapterAddress() + // ── Discovery ─────────────────────────────────────────────────── @Test fun testDiscoverPeripheralByName() = NativeTests.testDiscoverPeripheralByName() @Test fun testDiscoverServices() = NativeTests.testDiscoverServices() @@ -70,6 +73,9 @@ class BleIntegrationTest { @Test fun testAdvertisementManufacturerData() = NativeTests.testAdvertisementManufacturerData() @Test fun testAdvertisementServices() = NativeTests.testAdvertisementServices() + // ── Retrieval ──────────────────────────────────────────────────── + @Test fun testRetrievePeripheralsNotSupported() = NativeTests.testRetrievePeripheralsNotSupported() + // ── Connection ────────────────────────────────────────────────── @Test fun testConnectAndDisconnect() = NativeTests.testConnectAndDisconnect() @Test fun testReconnectAfterDisconnect() = NativeTests.testReconnectAfterDisconnect() @@ -97,7 +103,7 @@ class BleIntegrationTest { @Test fun testDescriptorDiscovery() = NativeTests.testDescriptorDiscovery() // ── Device Info ───────────────────────────────────────────────── - @Test fun testMtuAfterConnection() = NativeTests.testMtuAfterConnection() + @Test fun testMtuAfterServiceDiscovery() = NativeTests.testMtuAfterServiceDiscovery() @Test fun testReadRssi() = NativeTests.testReadRssi() @Test fun testPropertiesContainPeripheralInfo() = NativeTests.testPropertiesContainPeripheralInfo() @Test fun testConnectionParameters() = NativeTests.testConnectionParameters() diff --git a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt index ab43ee4e..8c0b8815 100644 --- a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt +++ b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt @@ -4,6 +4,9 @@ package com.nonpolynomial.btleplug.test object NativeTests { external fun initBtleplug() + // Adapter capabilities + external fun testAdapterAddress() + // Discovery external fun testDiscoverPeripheralByName() external fun testDiscoverServices() @@ -12,6 +15,9 @@ object NativeTests { external fun testAdvertisementManufacturerData() external fun testAdvertisementServices() + // Retrieval + external fun testRetrievePeripheralsNotSupported() + // Connection external fun testConnectAndDisconnect() external fun testReconnectAfterDisconnect() @@ -39,7 +45,7 @@ object NativeTests { external fun testDescriptorDiscovery() // Device Info - external fun testMtuAfterConnection() + external fun testMtuAfterServiceDiscovery() external fun testReadRssi() external fun testPropertiesContainPeripheralInfo() external fun testConnectionParameters() diff --git a/tests/common/gatt_uuids.rs b/tests/common/gatt_uuids.rs index 261b3e82..944edec5 100644 --- a/tests/common/gatt_uuids.rs +++ b/tests/common/gatt_uuids.rs @@ -41,4 +41,5 @@ pub const CMD_SET_NOTIFICATION_PAYLOAD: u8 = 0x06; // --- Test constants --- pub const STATIC_READ_VALUE: &[u8] = &[0x01, 0x02, 0x03, 0x04]; pub const TEST_PERIPHERAL_NAME: &str = "btleplug-test"; +pub const TEST_APPEARANCE: u16 = 0x0340; pub const MANUFACTURER_COMPANY_ID: u16 = 0xFFFF; diff --git a/tests/common/peripheral_finder.rs b/tests/common/peripheral_finder.rs index 3806c3cb..9cf1dd79 100644 --- a/tests/common/peripheral_finder.rs +++ b/tests/common/peripheral_finder.rs @@ -33,24 +33,39 @@ pub async fn get_adapter() -> &'static Adapter { std::thread::Builder::new() .name("btleplug-test-adapter".into()) .spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("failed to create adapter runtime"); - rt.block_on(async { - let manager = Manager::new().await.expect("failed to create BLE manager"); - let adapters = manager.adapters().await.expect("failed to get adapters"); - // Leak the manager so it (and the underlying CBCentralManager) - // lives forever. OnceCell keeps the Adapter alive; we need the - // Manager alive too since the Adapter borrows from it internally - // on some platforms. - std::mem::forget(manager); - let adapter = adapters.into_iter().next().expect("no BLE adapters found"); - tx.send(adapter).ok(); - // Block forever so the runtime (and its spawned event loop) - // stays alive. - std::future::pending::<()>().await; - }); + log::info!("adapter thread: starting"); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to create adapter runtime"); + rt.block_on(async { + log::info!("adapter thread: creating manager"); + let manager = + Manager::new().await.expect("failed to create BLE manager"); + log::info!("adapter thread: getting adapters"); + let adapters = + manager.adapters().await.expect("failed to get adapters"); + log::info!("adapter thread: got {} adapters", adapters.len()); + std::mem::forget(manager); + let adapter = + adapters.into_iter().next().expect("no BLE adapters found"); + log::info!("adapter thread: sending adapter"); + tx.send(adapter).ok(); + log::info!("adapter thread: blocking forever"); + std::future::pending::<()>().await; + }); + })); + if let Err(panic) = result { + let msg = if let Some(s) = panic.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = panic.downcast_ref::() { + s.clone() + } else { + "unknown panic".to_string() + }; + log::error!("adapter thread PANICKED: {}", msg); + } }) .expect("failed to spawn adapter thread"); rx.await diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 5915f5e4..20b2e6cb 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -3,11 +3,46 @@ //! Each function contains the actual test logic, callable from both //! desktop `#[tokio::test]` wrappers and Android JNI test harness. +#![allow(dead_code)] + use btleplug::api::Peripheral as _; use super::gatt_uuids; use super::peripheral_finder; +// ── Adapter capabilities ───────────────────────────────────────────── + +pub async fn test_adapter_address() { + use btleplug::api::{BDAddr, Central}; + + let adapter = peripheral_finder::get_adapter().await; + let result = adapter + .adapter_address() + .await + .expect("Failed to get adapter address"); + match &result { + Some(address) => assert_ne!( + *address, + BDAddr::default(), + "adapter address must be nonzero" + ), + None => { + #[cfg(any(target_os = "linux", target_os = "windows"))] + panic!("adapter address is unavailable on a supported desktop platform"); + #[cfg(any(target_vendor = "apple", target_os = "android"))] + return; + } + } + + #[cfg(target_os = "linux")] + if let Ok(expected) = std::env::var("BTLEPLUG_TEST_ADAPTER_ADDRESS") { + let expected = expected + .parse() + .expect("invalid BTLEPLUG_TEST_ADAPTER_ADDRESS"); + assert_eq!(Some(expected), result); + } +} + // ── Discovery ─────────────────────────────────────────────────────── pub async fn test_discover_peripheral_by_name() { @@ -33,6 +68,71 @@ pub async fn test_discover_peripheral_by_name() { peripheral.disconnect().await.unwrap(); } +#[cfg(target_os = "macos")] +pub async fn test_clear_peripherals_rediscovers_device() { + use btleplug::api::{Central, CentralEvent, ScanFilter}; + use futures::StreamExt; + use std::time::Duration; + use tokio::time; + + let adapter = peripheral_finder::get_adapter().await; + let peripheral_name = std::env::var("BTLEPLUG_TEST_PERIPHERAL") + .unwrap_or_else(|_| gatt_uuids::TEST_PERIPHERAL_NAME.to_string()); + let mut events = adapter.events().await.unwrap(); + + adapter.start_scan(ScanFilter::default()).await.unwrap(); + let peripheral = time::timeout(Duration::from_secs(15), async { + loop { + for peripheral in adapter.peripherals().await.unwrap() { + if peripheral + .properties() + .await + .unwrap() + .is_some_and(|properties| { + properties.local_name.as_deref() == Some(&peripheral_name) + }) + { + return peripheral; + } + } + let _ = events.next().await; + } + }) + .await + .expect("timed out waiting for initial peripheral discovery"); + let peripheral_id = peripheral.id(); + + adapter.stop_scan().await.unwrap(); + adapter.clear_peripherals().await.unwrap(); + assert!( + adapter.peripherals().await.unwrap().is_empty(), + "clear_peripherals returned before the public map was cleared" + ); + + adapter.start_scan(ScanFilter::default()).await.unwrap(); + time::timeout(Duration::from_secs(15), async { + loop { + if matches!(events.next().await, Some(CentralEvent::DeviceDiscovered(id)) if id == peripheral_id) + { + break; + } + } + }) + .await + .expect("timed out waiting for rediscovery after clear_peripherals"); + adapter.stop_scan().await.unwrap(); + + assert!( + adapter + .peripherals() + .await + .unwrap() + .iter() + .any(|peripheral| peripheral.id() == peripheral_id), + "rediscovered peripheral was not restored to the public map" + ); +} + pub async fn test_discover_services() { let peripheral = peripheral_finder::find_and_connect().await; let services = peripheral.services(); @@ -159,6 +259,44 @@ pub async fn test_advertisement_services() { ); } +// ── Retrieval ────────────────────────────────────────────────────── + +pub async fn test_retrieve_peripherals_not_supported() { + use btleplug::api::{Central, RetrievePeripheralsOptions}; + + let adapter = peripheral_finder::get_adapter().await; + let error = adapter + .retrieve_peripherals(RetrievePeripheralsOptions::default()) + .await + .expect_err("retrieval without selectors should not be supported on Android"); + assert!(matches!( + error, + btleplug::Error::NotSupported(operation) if operation == "retrieve_peripherals" + )); +} + +pub async fn test_retrieve_connected_peripheral_by_service() { + use btleplug::api::{Central, RetrievePeripheralsOptions}; + + let adapter = peripheral_finder::get_adapter().await; + let expected = peripheral_finder::find_and_connect().await; + let expected_id = expected.id(); + let retrieved = adapter + .retrieve_peripherals(RetrievePeripheralsOptions { + identifiers: None, + services: Some(vec![gatt_uuids::CONTROL_SERVICE]), + }) + .await + .expect("retrieval by service should be supported on desktop backends"); + assert!( + retrieved + .iter() + .any(|peripheral| peripheral.id() == expected_id), + "connected test peripheral was not returned by service retrieval" + ); + expected.disconnect().await.unwrap(); +} + // ── Connection ────────────────────────────────────────────────────── pub async fn test_connect_and_disconnect() { @@ -464,9 +602,40 @@ pub async fn test_unsubscribe_stops_notifications() { } assert!(got_one, "Should have received at least one notification"); + // Discard notifications that were already queued before unsubscribe. Stop + // draining once the stream is briefly quiet; do not wait indefinitely for + // a new item. + loop { + if time::timeout(Duration::from_millis(100), stream.next()) + .await + .is_err() + { + break; + } + } peripheral.unsubscribe(&char).await.unwrap(); - time::sleep(Duration::from_secs(2)).await; + let mut received_after_unsubscribe = false; + let timeout = time::sleep(Duration::from_secs(2)); + tokio::pin!(timeout); + loop { + tokio::select! { + Some(n) = stream.next() => { + if n.uuid == gatt_uuids::NOTIFY_CHAR { + received_after_unsubscribe = true; + break; + } + } + _ = &mut timeout => break, + } + } + assert!( + !received_after_unsubscribe, + "Should not receive notifications after unsubscribe" + ); + + // A second unsubscribe must be harmless and leave notifications disabled. + peripheral.unsubscribe(&char).await.unwrap(); peripheral_finder::send_control_command(&peripheral, gatt_uuids::CMD_STOP_NOTIFICATIONS).await; peripheral.disconnect().await.unwrap(); } @@ -581,9 +750,10 @@ pub async fn test_descriptor_discovery() { // ── Device Info ───────────────────────────────────────────────────── -pub async fn test_mtu_after_connection() { +pub async fn test_mtu_after_service_discovery() { let peripheral = peripheral_finder::find_and_connect().await; let mtu = peripheral.mtu(); + assert!( mtu >= 23, "MTU should be at least 23 (default), got {}", @@ -642,6 +812,17 @@ pub async fn test_properties_contain_peripheral_info() { props.tx_power_level.is_some(), "TX Power Level should be present in advertisement properties" ); + #[cfg(target_vendor = "apple")] + assert_eq!( + props.appearance, None, + "CoreBluetooth does not expose GAP Appearance advertising data" + ); + #[cfg(not(target_vendor = "apple"))] + assert_eq!( + props.appearance, + Some(gatt_uuids::TEST_APPEARANCE), + "Properties should contain the advertised GAP Appearance" + ); peripheral.disconnect().await.unwrap(); } diff --git a/tests/documentation.rs b/tests/documentation.rs new file mode 100644 index 00000000..2d04ceeb --- /dev/null +++ b/tests/documentation.rs @@ -0,0 +1,30 @@ +#[test] +fn readme_distinguishes_adapter_and_peripheral_addresses() { + let readme = include_str!("../README.md"); + assert!(readme.contains("Retrieve local adapter address")); + assert!(readme.contains( + "| Retrieve local adapter address | X | | X | |" + )); + assert!(readme.contains("Discover MAC address")); + assert!(readme.contains("PeripheralId")); + assert!(readme.contains("distinct from a discovered peripheral address")); +} + +#[test] +fn central_adapter_address_default_is_source_compatible() { + fn assert_default() {} + assert_default::(); +} + +#[test] +fn properties_and_event_example_document_current_async_contract() { + let api = include_str!("../src/api/mod.rs"); + assert!(api.contains("`Ok(Some(_))` contains a snapshot")); + assert!(api.contains("`Ok(None)` means that the backend has no properties snapshot")); + + let example = include_str!("../examples/event_driven_discovery.rs"); + assert!(example.contains("Process events asynchronously")); + assert!(example.contains("Tokio task")); + assert!(!example.contains("event receiver blocks")); + assert!(!example.contains("does not yet use async channels")); +} diff --git a/tests/test_adapter_address.rs b/tests/test_adapter_address.rs new file mode 100644 index 00000000..f027c4df --- /dev/null +++ b/tests/test_adapter_address.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires a local Bluetooth adapter"] +async fn test_adapter_address() { + common::test_cases::test_adapter_address().await; +} diff --git a/tests/test_clear_peripherals_rediscovers_device.rs b/tests/test_clear_peripherals_rediscovers_device.rs new file mode 100644 index 00000000..e4706d85 --- /dev/null +++ b/tests/test_clear_peripherals_rediscovers_device.rs @@ -0,0 +1,8 @@ +mod common; + +#[cfg(target_os = "macos")] +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_clear_peripherals_rediscovers_device() { + common::test_cases::test_clear_peripherals_rediscovers_device().await; +} diff --git a/tests/test_mtu_after_connection.rs b/tests/test_mtu_after_connection.rs deleted file mode 100644 index 50f549b1..00000000 --- a/tests/test_mtu_after_connection.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod common; - -#[tokio::test] -#[ignore = "requires BLE test peripheral"] -async fn test_mtu_after_connection() { - common::test_cases::test_mtu_after_connection().await; -} diff --git a/tests/test_mtu_after_service_discovery.rs b/tests/test_mtu_after_service_discovery.rs new file mode 100644 index 00000000..603ea1eb --- /dev/null +++ b/tests/test_mtu_after_service_discovery.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_mtu_after_service_discovery() { + common::test_cases::test_mtu_after_service_discovery().await; +} diff --git a/tests/test_retrieve_connected_peripheral_by_service.rs b/tests/test_retrieve_connected_peripheral_by_service.rs new file mode 100644 index 00000000..e5d3db11 --- /dev/null +++ b/tests/test_retrieve_connected_peripheral_by_service.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_retrieve_connected_peripheral_by_service() { + common::test_cases::test_retrieve_connected_peripheral_by_service().await; +}