diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b29d16f9..24e0da2a 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -47,9 +47,11 @@ jobs: submodules: recursive - name: Build FPP run: cargo build --manifest-path presence/fpp/fpp/Cargo.toml + - name: Build Bluetooth Module + run: cargo build --manifest-path fastpair/rust/bluetooth/Cargo.toml - name: Build Fast Pair - run: cargo build --manifest-path fastpair/rust/Cargo.toml - + run: cargo build --manifest-path fastpair/rust/demo/rust/Cargo.toml + build-rust-windows: name: Build Rust on Windows runs-on: windows-latest @@ -57,6 +59,8 @@ jobs: - uses: actions/checkout@v3 with: submodules: recursive + - name: Build Bluetooth Module + run: cargo build --manifest-path fastpair/rust/bluetooth/Cargo.toml - name: Build Fast Pair - run: cargo build --manifest-path fastpair/rust/Cargo.toml + run: cargo build --manifest-path fastpair/rust/demo/rust/Cargo.toml \ No newline at end of file diff --git a/fastpair/rust/Cargo.toml b/fastpair/rust/bluetooth/Cargo.toml similarity index 94% rename from fastpair/rust/Cargo.toml rename to fastpair/rust/bluetooth/Cargo.toml index 02b48ebc..0c666219 100644 --- a/fastpair/rust/Cargo.toml +++ b/fastpair/rust/bluetooth/Cargo.toml @@ -13,19 +13,22 @@ # limitations under the License. [package] -name = "fastpair" +name = "bluetooth" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -futures = { version = "0.3", features = ["executor"] } +futures = { version = "0.3" } tracing = "0.1.37" cfg-if = "1.0.0" async-trait = "0.1" thiserror = "1.0.43" +[dev-dependencies] +futures = { version = "0.3", features = ["executor"] } + [target.'cfg(windows)'.dependencies] windows = { version = "0.48", features = [ "Devices_Bluetooth", diff --git a/fastpair/rust/src/main.rs b/fastpair/rust/bluetooth/examples/fastpair_ui.rs similarity index 96% rename from fastpair/rust/src/main.rs rename to fastpair/rust/bluetooth/examples/fastpair_ui.rs index 19d77c00..920ace4c 100644 --- a/fastpair/rust/src/main.rs +++ b/fastpair/rust/bluetooth/examples/fastpair_ui.rs @@ -25,11 +25,11 @@ use futures::{ lock::Mutex, }; -mod bluetooth; +extern crate bluetooth; -use crate::bluetooth::{ - BleAdapter, BleDataTypeId, BleDevice, ClassicAddress, ClassicDevice, - Platform, +use bluetooth::{ + api::{BleAdapter, BleDevice, ClassicDevice}, + BleDataTypeId, ClassicAddress, Platform, }; async fn get_user_input( diff --git a/fastpair/rust/rustfmt.toml b/fastpair/rust/bluetooth/rustfmt.toml similarity index 100% rename from fastpair/rust/rustfmt.toml rename to fastpair/rust/bluetooth/rustfmt.toml diff --git a/fastpair/rust/src/bluetooth/api/adapter.rs b/fastpair/rust/bluetooth/src/api/adapter.rs similarity index 93% rename from fastpair/rust/src/bluetooth/api/adapter.rs rename to fastpair/rust/bluetooth/src/api/adapter.rs index 39ab9310..de9bc43f 100644 --- a/fastpair/rust/src/bluetooth/api/adapter.rs +++ b/fastpair/rust/bluetooth/src/api/adapter.rs @@ -14,9 +14,7 @@ use async_trait::async_trait; -use crate::bluetooth::common::{ - BleAdvertisement, BleDataTypeId, BluetoothError, -}; +use crate::common::{BleAdvertisement, BleDataTypeId, BluetoothError}; /// Concrete types implementing this trait are Bluetooth Central devices. /// They provide methods for retrieving nearby connections and device info. diff --git a/fastpair/rust/src/bluetooth/api/device.rs b/fastpair/rust/bluetooth/src/api/device.rs similarity index 98% rename from fastpair/rust/src/bluetooth/api/device.rs rename to fastpair/rust/bluetooth/src/api/device.rs index 97cba1d8..df43ec72 100644 --- a/fastpair/rust/src/bluetooth/api/device.rs +++ b/fastpair/rust/bluetooth/src/api/device.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; -use crate::bluetooth::common::{ +use crate::common::{ BleAddress, BluetoothError, ClassicAddress, PairingResult, }; diff --git a/fastpair/rust/src/bluetooth/api/mod.rs b/fastpair/rust/bluetooth/src/api/mod.rs similarity index 100% rename from fastpair/rust/src/bluetooth/api/mod.rs rename to fastpair/rust/bluetooth/src/api/mod.rs diff --git a/fastpair/rust/src/bluetooth/common/address.rs b/fastpair/rust/bluetooth/src/common/address.rs similarity index 98% rename from fastpair/rust/src/bluetooth/common/address.rs rename to fastpair/rust/bluetooth/src/common/address.rs index 485ff731..db0481f6 100644 --- a/fastpair/rust/src/bluetooth/common/address.rs +++ b/fastpair/rust/bluetooth/src/common/address.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::bluetooth::common::BluetoothError; +use super::BluetoothError; /// BLE Addresses can either be the peripheral's public MAC address, or various /// types of random addresses. diff --git a/fastpair/rust/src/bluetooth/common/advertisement.rs b/fastpair/rust/bluetooth/src/common/advertisement.rs similarity index 71% rename from fastpair/rust/src/bluetooth/common/advertisement.rs rename to fastpair/rust/bluetooth/src/common/advertisement.rs index 8260936e..9fb7f999 100644 --- a/fastpair/rust/src/bluetooth/common/advertisement.rs +++ b/fastpair/rust/bluetooth/src/common/advertisement.rs @@ -18,16 +18,30 @@ use super::{BleAddress, BluetoothError}; /// information about the advertisement (e.g. address of sender) as well as /// data sections extracted from the advertisement. Platform-specific methods /// should be written to load in data sections from incoming advertisements. +#[derive(Clone)] pub struct BleAdvertisement { address: BleAddress, + rssi: Option, + tx_power: Option, service_data_16bit_uuid: Option>>, } +/// Decibel-milliwatt or dBm is a dimensionless absolute unit expressing the +/// power of a signal relative to one milliwatt (mW). The unit is in log10, i.e. +/// 1 mW is 0 dBm and a 10 dBm increase represents a ten-fold increase in power. +type DecibelMilliwatts = i16; + impl BleAdvertisement { /// Construct a new `BleAdvertisement` instance. - pub(crate) fn new(address: BleAddress) -> Self { + pub(crate) fn new( + address: BleAddress, + rssi: Option, + tx_power: Option, + ) -> Self { BleAdvertisement { address, + rssi, + tx_power, service_data_16bit_uuid: None, } } @@ -37,6 +51,19 @@ impl BleAdvertisement { self.address } + /// Retrieve the Received Signal Strength Indicator (RSSI) value for this + /// advertisement, expressed in dBm. The RSSI might be the raw value or the + /// filtered RSSI, depending on the configured signal strength filter. + pub fn rssi(&self) -> Option { + self.rssi + } + + /// Retrieve the transmit power advertised by this device, if any. + /// For BLE communication, values will range from -127 dBm to 20 dBm. + pub fn tx_power(&self) -> Option { + self.tx_power + } + /// Setter for `ServiceData` field with 16bit UUID. pub(crate) fn set_service_data_16bit_uuid( &mut self, @@ -69,6 +96,7 @@ pub enum BleDataTypeId { /// Struct representing the Bluetooth Service Data common data type. `U` should /// be one of the valid uuid sizes, specified in: /// Bluetooth Supplement to the Core Specification, Part A, Section 1.11. +#[derive(Clone)] pub struct ServiceData { uuid: U, data: Vec, diff --git a/fastpair/rust/src/bluetooth/common/error.rs b/fastpair/rust/bluetooth/src/common/error.rs similarity index 100% rename from fastpair/rust/src/bluetooth/common/error.rs rename to fastpair/rust/bluetooth/src/common/error.rs diff --git a/fastpair/rust/src/bluetooth/common/mod.rs b/fastpair/rust/bluetooth/src/common/mod.rs similarity index 100% rename from fastpair/rust/src/bluetooth/common/mod.rs rename to fastpair/rust/bluetooth/src/common/mod.rs diff --git a/fastpair/rust/src/bluetooth/mod.rs b/fastpair/rust/bluetooth/src/lib.rs similarity index 87% rename from fastpair/rust/src/bluetooth/mod.rs rename to fastpair/rust/bluetooth/src/lib.rs index 0acdb250..ebe8384a 100644 --- a/fastpair/rust/src/bluetooth/mod.rs +++ b/fastpair/rust/bluetooth/src/lib.rs @@ -12,16 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Split into separate crate once demo is finished, providing custom error types -// instead of using anyhow. -// b/290070686 - pub mod api; -pub mod common; +mod common; -pub use api::{BleAdapter, BleDevice, ClassicDevice}; +use api::{BleAdapter, BleDevice, ClassicDevice}; pub use common::{ - BleAddress, BleAdvertisement, BleDataTypeId, BluetoothError, ClassicAddress, + BleAddress, BleAdvertisement, BleDataTypeId, BluetoothError, + ClassicAddress, PairingResult, ServiceData, }; cfg_if::cfg_if! { diff --git a/fastpair/rust/src/message_stream.rs b/fastpair/rust/bluetooth/src/message_stream.rs similarity index 100% rename from fastpair/rust/src/message_stream.rs rename to fastpair/rust/bluetooth/src/message_stream.rs diff --git a/fastpair/rust/src/types.rs b/fastpair/rust/bluetooth/src/types.rs similarity index 100% rename from fastpair/rust/src/types.rs rename to fastpair/rust/bluetooth/src/types.rs diff --git a/fastpair/rust/src/types/packets.rs b/fastpair/rust/bluetooth/src/types/packets.rs similarity index 100% rename from fastpair/rust/src/types/packets.rs rename to fastpair/rust/bluetooth/src/types/packets.rs diff --git a/fastpair/rust/src/bluetooth/unsupported/adapter.rs b/fastpair/rust/bluetooth/src/unsupported/adapter.rs similarity index 93% rename from fastpair/rust/src/bluetooth/unsupported/adapter.rs rename to fastpair/rust/bluetooth/src/unsupported/adapter.rs index de883e3c..7ae210e1 100644 --- a/fastpair/rust/src/bluetooth/unsupported/adapter.rs +++ b/fastpair/rust/bluetooth/src/unsupported/adapter.rs @@ -15,9 +15,7 @@ use async_trait::async_trait; use super::BleDevice; -use crate::bluetooth::{ - api, common::BluetoothError, BleAdvertisement, BleDataTypeId, -}; +use crate::{api, common::BluetoothError, BleAdvertisement, BleDataTypeId}; /// Concrete type implementing `Adapter`, used for unsupported devices. /// Every method should panic. diff --git a/fastpair/rust/src/bluetooth/unsupported/device.rs b/fastpair/rust/bluetooth/src/unsupported/device.rs similarity index 98% rename from fastpair/rust/src/bluetooth/unsupported/device.rs rename to fastpair/rust/bluetooth/src/unsupported/device.rs index 0ac68775..244ea06c 100644 --- a/fastpair/rust/src/bluetooth/unsupported/device.rs +++ b/fastpair/rust/bluetooth/src/unsupported/device.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; -use crate::bluetooth::{ +use crate::{ api, common::{BleAddress, BluetoothError, ClassicAddress, PairingResult}, }; diff --git a/fastpair/rust/src/bluetooth/unsupported/mod.rs b/fastpair/rust/bluetooth/src/unsupported/mod.rs similarity index 100% rename from fastpair/rust/src/bluetooth/unsupported/mod.rs rename to fastpair/rust/bluetooth/src/unsupported/mod.rs diff --git a/fastpair/rust/src/bluetooth/windows/adapter.rs b/fastpair/rust/bluetooth/src/windows/adapter.rs similarity index 99% rename from fastpair/rust/src/bluetooth/windows/adapter.rs rename to fastpair/rust/bluetooth/src/windows/adapter.rs index cf4519f6..7130cb97 100644 --- a/fastpair/rust/src/bluetooth/windows/adapter.rs +++ b/fastpair/rust/bluetooth/src/windows/adapter.rs @@ -53,7 +53,7 @@ use windows::{ Foundation::TypedEventHandler, }; -use crate::bluetooth::{ +use crate::{ api, common::{BleAdvertisement, BleDataTypeId, BluetoothError}, }; diff --git a/fastpair/rust/src/bluetooth/windows/address.rs b/fastpair/rust/bluetooth/src/windows/address.rs similarity index 97% rename from fastpair/rust/src/bluetooth/windows/address.rs rename to fastpair/rust/bluetooth/src/windows/address.rs index d08ae4f3..5423c429 100644 --- a/fastpair/rust/src/bluetooth/windows/address.rs +++ b/fastpair/rust/bluetooth/src/windows/address.rs @@ -16,7 +16,7 @@ //https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621 use windows::Devices::Bluetooth::BluetoothAddressType; -use crate::bluetooth::common::{BleAddressKind, BluetoothError}; +use crate::common::{BleAddressKind, BluetoothError}; // Convenience for converting from Windows API to crate API. impl TryFrom for BleAddressKind { diff --git a/fastpair/rust/src/bluetooth/windows/advertisement.rs b/fastpair/rust/bluetooth/src/windows/advertisement.rs similarity index 90% rename from fastpair/rust/src/bluetooth/windows/advertisement.rs rename to fastpair/rust/bluetooth/src/windows/advertisement.rs index 526e2d5a..bf80b5a3 100644 --- a/fastpair/rust/src/bluetooth/windows/advertisement.rs +++ b/fastpair/rust/bluetooth/src/windows/advertisement.rs @@ -29,7 +29,7 @@ use windows::{ Storage::Streams::DataReader, }; -use crate::bluetooth::common::{ +use crate::common::{ BleAddress, BleAddressKind, BleAdvertisement, BleDataTypeId, BluetoothError, ServiceData, }; @@ -44,8 +44,15 @@ impl TryFrom<&BluetoothLEAdvertisementReceivedEventArgs> for BleAdvertisement { let kind = BleAddressKind::try_from(adv.BluetoothAddressType()?)?; let addr = BleAddress::new(addr, kind); + // `rssi` and tx_power` aren't always advertised, so convert to None if + // can't extract value. + let rssi = adv.RawSignalStrengthInDBm().ok(); + let tx_power = match adv.TransmitPowerLevelInDBm() { + Ok(val_ref) => val_ref.GetInt16().ok(), + Err(_) => None, + }; - Ok(BleAdvertisement::new(addr)) + Ok(BleAdvertisement::new(addr, rssi, tx_power)) } } diff --git a/fastpair/rust/src/bluetooth/windows/device.rs b/fastpair/rust/bluetooth/src/windows/device.rs similarity index 98% rename from fastpair/rust/src/bluetooth/windows/device.rs rename to fastpair/rust/bluetooth/src/windows/device.rs index cf0e293f..a3d2d649 100644 --- a/fastpair/rust/src/bluetooth/windows/device.rs +++ b/fastpair/rust/bluetooth/src/windows/device.rs @@ -49,7 +49,7 @@ use windows::{ Foundation::TypedEventHandler, }; -use crate::bluetooth::{api, common::{BleAddress, ClassicAddress, BluetoothError, PairingResult}}; +use crate::{api, common::{BleAddress, ClassicAddress, BluetoothError, PairingResult}}; /// Concrete type implementing `Device`, used for Windows BLE. pub struct BleDevice { diff --git a/fastpair/rust/src/bluetooth/windows/error.rs b/fastpair/rust/bluetooth/src/windows/error.rs similarity index 98% rename from fastpair/rust/src/bluetooth/windows/error.rs rename to fastpair/rust/bluetooth/src/windows/error.rs index 59e12a31..9a7f6d4c 100644 --- a/fastpair/rust/src/bluetooth/windows/error.rs +++ b/fastpair/rust/bluetooth/src/windows/error.rs @@ -14,7 +14,7 @@ use windows::Devices::Enumeration::DevicePairingResultStatus; -use crate::bluetooth::common::{BluetoothError, PairingResult}; +use crate::common::{BluetoothError, PairingResult}; impl From for BluetoothError { fn from(err: windows::core::Error) -> Self { diff --git a/fastpair/rust/src/bluetooth/windows/mod.rs b/fastpair/rust/bluetooth/src/windows/mod.rs similarity index 100% rename from fastpair/rust/src/bluetooth/windows/mod.rs rename to fastpair/rust/bluetooth/src/windows/mod.rs diff --git a/fastpair/rust/tests/integration_test.rs b/fastpair/rust/bluetooth/tests/integration_test.rs similarity index 97% rename from fastpair/rust/tests/integration_test.rs rename to fastpair/rust/bluetooth/tests/integration_test.rs index 18021531..b3332b2a 100644 --- a/fastpair/rust/tests/integration_test.rs +++ b/fastpair/rust/bluetooth/tests/integration_test.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use fastpair::*; +use bluetooth::*; mod tests { use super::*; diff --git a/fastpair/rust/demo/.gitignore b/fastpair/rust/demo/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/fastpair/rust/demo/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/fastpair/rust/demo/.metadata b/fastpair/rust/demo/.metadata new file mode 100644 index 00000000..de745e4a --- /dev/null +++ b/fastpair/rust/demo/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: linux + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: web + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: windows + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/fastpair/rust/demo/README.md b/fastpair/rust/demo/README.md new file mode 100644 index 00000000..dbd403a0 --- /dev/null +++ b/fastpair/rust/demo/README.md @@ -0,0 +1,16 @@ +# demo + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/fastpair/rust/demo/analysis_options.yaml b/fastpair/rust/demo/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/fastpair/rust/demo/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/fastpair/rust/demo/bindgen.script b/fastpair/rust/demo/bindgen.script new file mode 100644 index 00000000..4533096a --- /dev/null +++ b/fastpair/rust/demo/bindgen.script @@ -0,0 +1,4 @@ +flutter_rust_bridge_codegen --rust-input rust/src/api.rs --dart-output lib/bridge_generated.dart --dart-decl-output lib/bridge_definitions.dart + +# github.com/google/addlicense +addlicense . \ No newline at end of file diff --git a/fastpair/rust/demo/lib/bridge_definitions.dart b/fastpair/rust/demo/lib/bridge_definitions.dart new file mode 100644 index 00000000..67d21a9c --- /dev/null +++ b/fastpair/rust/demo/lib/bridge_definitions.dart @@ -0,0 +1,56 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`@ 1.79.0. +// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member, prefer_is_empty, unnecessary_const + +import 'dart:convert'; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'package:uuid/uuid.dart'; + +import 'package:collection/collection.dart'; + +abstract class Rust { + /// Sets up initial constructs and infinitely polls for advertisements. + Future init({dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kInitConstMeta; + + /// Sets up `StreamSink` for Dart-Rust FFI. + Stream eventStream({dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kEventStreamConstMeta; + + /// Attempt classic pairing with currently displayed device. + Future pair({dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kPairConstMeta; + + /// Remove this device from display and add it to the TTL cache blacklist. + Future dismiss({dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kDismissConstMeta; +} + +class StringArray2 extends NonGrowableListView { + static const arraySize = 2; + StringArray2(List inner) + : assert(inner.length == arraySize), + super(inner); + StringArray2.unchecked(List inner) : super(inner); + StringArray2.init(String fill) : super(List.filled(arraySize, fill)); +} diff --git a/fastpair/rust/demo/lib/bridge_generated.dart b/fastpair/rust/demo/lib/bridge_generated.dart new file mode 100644 index 00000000..72999e9a --- /dev/null +++ b/fastpair/rust/demo/lib/bridge_generated.dart @@ -0,0 +1,319 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`@ 1.79.0. +// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, unnecessary_import, prefer_single_quotes, prefer_const_constructors, use_super_parameters, always_use_package_imports, annotate_overrides, invalid_use_of_protected_member, constant_identifier_names, invalid_use_of_internal_member, prefer_is_empty, unnecessary_const + +import "bridge_definitions.dart"; +import 'dart:convert'; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'package:uuid/uuid.dart'; + +import 'dart:convert'; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'package:uuid/uuid.dart'; + +import 'dart:ffi' as ffi; + +class RustImpl implements Rust { + final RustPlatform _platform; + factory RustImpl(ExternalLibrary dylib) => RustImpl.raw(RustPlatform(dylib)); + + /// Only valid on web/WASM platforms. + factory RustImpl.wasm(FutureOr module) => + RustImpl(module as ExternalLibrary); + RustImpl.raw(this._platform); + Future init({dynamic hint}) { + return _platform.executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_init(port_), + parseSuccessData: _wire2api_unit, + constMeta: kInitConstMeta, + argValues: [], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kInitConstMeta => + const FlutterRustBridgeTaskConstMeta( + debugName: "init", + argNames: [], + ); + + Stream eventStream({dynamic hint}) { + return _platform.executeStream(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_event_stream(port_), + parseSuccessData: _wire2api_opt_String_array_2, + constMeta: kEventStreamConstMeta, + argValues: [], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kEventStreamConstMeta => + const FlutterRustBridgeTaskConstMeta( + debugName: "event_stream", + argNames: [], + ); + + Future pair({dynamic hint}) { + return _platform.executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_pair(port_), + parseSuccessData: _wire2api_String, + constMeta: kPairConstMeta, + argValues: [], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kPairConstMeta => + const FlutterRustBridgeTaskConstMeta( + debugName: "pair", + argNames: [], + ); + + Future dismiss({dynamic hint}) { + return _platform.executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => _platform.inner.wire_dismiss(port_), + parseSuccessData: _wire2api_unit, + constMeta: kDismissConstMeta, + argValues: [], + hint: hint, + )); + } + + FlutterRustBridgeTaskConstMeta get kDismissConstMeta => + const FlutterRustBridgeTaskConstMeta( + debugName: "dismiss", + argNames: [], + ); + + void dispose() { + _platform.dispose(); + } +// Section: wire2api + + String _wire2api_String(dynamic raw) { + return raw as String; + } + + StringArray2 _wire2api_String_array_2(dynamic raw) { + return StringArray2((raw as List).map(_wire2api_String).toList()); + } + + List _wire2api_list_String(dynamic raw) { + return (raw as List).map(_wire2api_String).toList(); + } + + StringArray2? _wire2api_opt_String_array_2(dynamic raw) { + return raw == null ? null : _wire2api_String_array_2(raw); + } + + int _wire2api_u8(dynamic raw) { + return raw as int; + } + + Uint8List _wire2api_uint_8_list(dynamic raw) { + return raw as Uint8List; + } + + void _wire2api_unit(dynamic raw) { + return; + } +} + +// Section: api2wire + +// Section: finalizer + +class RustPlatform extends FlutterRustBridgeBase { + RustPlatform(ffi.DynamicLibrary dylib) : super(RustWire(dylib)); + +// Section: api2wire + +// Section: finalizer + +// Section: api_fill_to_wire +} + +// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint + +/// generated by flutter_rust_bridge +class RustWire implements FlutterRustBridgeWireBase { + @internal + late final dartApi = DartApiDl(init_frb_dart_api_dl); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustWire(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + RustWire.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + void store_dart_post_cobject( + DartPostCObjectFnType ptr, + ) { + return _store_dart_post_cobject( + ptr, + ); + } + + late final _store_dart_post_cobjectPtr = + _lookup>( + 'store_dart_post_cobject'); + late final _store_dart_post_cobject = _store_dart_post_cobjectPtr + .asFunction(); + + Object get_dart_object( + int ptr, + ) { + return _get_dart_object( + ptr, + ); + } + + late final _get_dart_objectPtr = + _lookup>( + 'get_dart_object'); + late final _get_dart_object = + _get_dart_objectPtr.asFunction(); + + void drop_dart_object( + int ptr, + ) { + return _drop_dart_object( + ptr, + ); + } + + late final _drop_dart_objectPtr = + _lookup>( + 'drop_dart_object'); + late final _drop_dart_object = + _drop_dart_objectPtr.asFunction(); + + int new_dart_opaque( + Object handle, + ) { + return _new_dart_opaque( + handle, + ); + } + + late final _new_dart_opaquePtr = + _lookup>( + 'new_dart_opaque'); + late final _new_dart_opaque = + _new_dart_opaquePtr.asFunction(); + + int init_frb_dart_api_dl( + ffi.Pointer obj, + ) { + return _init_frb_dart_api_dl( + obj, + ); + } + + late final _init_frb_dart_api_dlPtr = + _lookup)>>( + 'init_frb_dart_api_dl'); + late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr + .asFunction)>(); + + void wire_init( + int port_, + ) { + return _wire_init( + port_, + ); + } + + late final _wire_initPtr = + _lookup>('wire_init'); + late final _wire_init = _wire_initPtr.asFunction(); + + void wire_event_stream( + int port_, + ) { + return _wire_event_stream( + port_, + ); + } + + late final _wire_event_streamPtr = + _lookup>( + 'wire_event_stream'); + late final _wire_event_stream = + _wire_event_streamPtr.asFunction(); + + void wire_pair( + int port_, + ) { + return _wire_pair( + port_, + ); + } + + late final _wire_pairPtr = + _lookup>('wire_pair'); + late final _wire_pair = _wire_pairPtr.asFunction(); + + void wire_dismiss( + int port_, + ) { + return _wire_dismiss( + port_, + ); + } + + late final _wire_dismissPtr = + _lookup>('wire_dismiss'); + late final _wire_dismiss = _wire_dismissPtr.asFunction(); + + void free_WireSyncReturn( + WireSyncReturn ptr, + ) { + return _free_WireSyncReturn( + ptr, + ); + } + + late final _free_WireSyncReturnPtr = + _lookup>( + 'free_WireSyncReturn'); + late final _free_WireSyncReturn = + _free_WireSyncReturnPtr.asFunction(); +} + +final class _Dart_Handle extends ffi.Opaque {} + +typedef DartPostCObjectFnType = ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function(DartPort port_id, ffi.Pointer message)>>; +typedef DartPort = ffi.Int64; diff --git a/fastpair/rust/demo/lib/main.dart b/fastpair/rust/demo/lib/main.dart new file mode 100644 index 00000000..a9995d71 --- /dev/null +++ b/fastpair/rust/demo/lib/main.dart @@ -0,0 +1,130 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:demo/rust.dart'; + +void main() { + api.init(); + runApp(const FastPairApp()); +} + +class FastPairApp extends StatelessWidget { + const FastPairApp({super.key}); + @override + Widget build(BuildContext context) => MaterialApp( + title: 'Fast Pair', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const HomePage(), + ); +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text("Fast Pair"), + ), + body: Center( + child: StreamBuilder( + // Retrieve device info stream from Rust side. + stream: api.eventStream(), + builder: (context, deviceInfo) { + var deviceName = deviceInfo.data?[0]; + var deviceImageUrl = deviceInfo.data?[1]; + + if (deviceInfo.hasData && + deviceName != null && + deviceImageUrl != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Image.network(deviceImageUrl, + fit: BoxFit.contain)), + Text(deviceName), + // Spacing between device name text and buttons. + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Spacing between left edge of screen and first button. + const SizedBox(width: 20), + OutlinedButton( + // Invoke pairing dialog. + onPressed: () => pairing(context), + child: const Text('Pair'), + ), + // Spacing between buttons. + const SizedBox(width: 20), + OutlinedButton( + onPressed: () => api.dismiss(), + child: const Text('Dismiss')) + ], + ), + // Spacing between buttons and bottom of screen. + const SizedBox(height: 20), + ]); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ), + ), + ); +} + +// Displays pairing dialog box. +Future pairing(BuildContext context) => showDialog( + context: context, + // Rust functions are invoked as futures. + builder: (context) => FutureBuilder( + future: api.pair(), + builder: (context, pairResult) { + var pairResultValue = pairResult.data; + + return pairResult.hasData && pairResultValue != null + ? AlertDialog( + title: const Text('Pairing result'), + content: Text(pairResultValue), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ) + ], + ) + : const AlertDialog( + title: Text('Pairing...'), + // Ensures the progress indicator has sensible dimensions, + // otherwise it follows the height/width of the alert dialog. + content: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 50, + height: 50, + child: CircularProgressIndicator(), + ), + ], + ), + ); + })); diff --git a/fastpair/rust/demo/lib/rust.dart b/fastpair/rust/demo/lib/rust.dart new file mode 100644 index 00000000..a525483b --- /dev/null +++ b/fastpair/rust/demo/lib/rust.dart @@ -0,0 +1,32 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file initializes the dynamic library and connects it with the stub +// generated by flutter_rust_bridge_codegen. + +import 'dart:ffi'; + +import 'dart:io' as io; + +import 'package:demo/bridge_generated.dart'; + +const _base = 'rust'; + +// On MacOS, the dynamic library is not bundled with the binary, +// but rather directly **linked** against the binary. +final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so'; + +final api = RustImpl(io.Platform.isIOS || io.Platform.isMacOS + ? DynamicLibrary.executable() + : DynamicLibrary.open(_dylib)); diff --git a/fastpair/rust/demo/local/525296.json b/fastpair/rust/demo/local/525296.json new file mode 100644 index 00000000..0404cf7e --- /dev/null +++ b/fastpair/rust/demo/local/525296.json @@ -0,0 +1,40 @@ +{ + "device": { + "id": "525296", + "notificationType": "FAST_PAIR_ONE", + "imageUrl": "https://lh3.googleusercontent.com/M67kfg-lVtqeP0-CLuvR68J4MlY9wixO0Za3urah_5axGRUyi20KSEQiqjvhqxCWTxpsicU1w-TCL3BX", + "name": "LG HBS-1125", + "intentUri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;S.com.google.android.gms.nearby.discovery%3AEXTRA_COMPANION_APP=com.lge.tonentalkplus.tonentalkfree;end", + "triggerDistance": 0.6, + "antiSpoofingKeyPair": {}, + "status": { + "statusType": "PUBLISHED" + }, + "lastUpdateTimestamp": "2021-12-02T07:11:36.110Z", + "deviceType": "HEADPHONES", + "trueWirelessImages": {}, + "companyName": "LG", + "interactionType": "NOTIFICATION", + "companionDetail": {} + }, + "image": "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", + "strings": { + "initialNotificationDescription": "Tap to pair with this device", + "openCompanionAppDescription": "Tap to finish setup", + "updateCompanionAppDescription": "Tap to update device settings and finish setup", + "downloadCompanionAppDescription": "Tap to download device app on Google Play and see all features", + "unableToConnectTitle": "Unable to connect", + "unableToConnectDescription": "Try manually pairing to the device", + "initialPairingDescription": "%s will appear on devices linked with %s", + "connectSuccessCompanionAppInstalled": "Your device is ready to be set up", + "connectSuccessCompanionAppNotInstalled": "Download the device app on Google Play to see all available features", + "subsequentPairingDescription": "Connect %s to this phone", + "retroactivePairingDescription": "Save device to %s to connect more quickly to your other devices", + "waitLaunchCompanionAppDescription": "This will take a few moments", + "failConnectGoToSettingsDescription": "Try manually pairing to the device by going to Settings", + "assistantSetupHalfSheet": "Get hands-free help on the go from Google Assistant", + "assistantSetupNotification": "Tap to set up your Google Assistant", + "fastPairTvConnectDeviceNoAccountDescription": "Connect your %s with this device", + "subsequentPairingDescriptionOnTv": "Connect %s to TV" + } +} diff --git a/fastpair/rust/demo/local/706908.json b/fastpair/rust/demo/local/706908.json new file mode 100644 index 00000000..3a77259c --- /dev/null +++ b/fastpair/rust/demo/local/706908.json @@ -0,0 +1,40 @@ +{ + "device": { + "id": "706908", + "notificationType": "FAST_PAIR_ONE", + "imageUrl": "https://lh3.googleusercontent.com/9lYIq9GW5_tZ1WaTYsrU7NMc5MP8AgOcsHB5K75MlfeqhwIgm4jL_ilMtP9aLYEZR_6jx4rLI2-2-uYDjg", + "name": "Sony WH-1000XM3", + "intentUri": "intent:#Intent;action=com.google.android.gms.nearby.discovery%3AACTION_MAGIC_PAIR;package=com.google.android.gms;component=com.google.android.gms/.nearby.discovery.service.DiscoveryService;end", + "triggerDistance": 0.6, + "antiSpoofingKeyPair": {}, + "status": { + "statusType": "PUBLISHED" + }, + "lastUpdateTimestamp": "2020-05-19T17:58:19.881Z", + "deviceType": "HEADPHONES", + "trueWirelessImages": {}, + "companyName": "Sony", + "interactionType": "NOTIFICATION", + "companionDetail": {} + }, + "image": "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", + "strings": { + "initialNotificationDescription": "Tap to pair with this device", + "openCompanionAppDescription": "Tap to finish setup", + "updateCompanionAppDescription": "Tap to update device settings and finish setup", + "downloadCompanionAppDescription": "Tap to download device app on Google Play and see all features", + "unableToConnectTitle": "Unable to connect", + "unableToConnectDescription": "Try manually pairing to the device", + "initialPairingDescription": "%s will appear on devices linked with %s", + "connectSuccessCompanionAppInstalled": "Your device is ready to be set up", + "connectSuccessCompanionAppNotInstalled": "Download the device app on Google Play to see all available features", + "subsequentPairingDescription": "Connect %s to this phone", + "retroactivePairingDescription": "Save device to %s to connect more quickly to your other devices", + "waitLaunchCompanionAppDescription": "This will take a few moments", + "failConnectGoToSettingsDescription": "Try manually pairing to the device by going to Settings", + "assistantSetupHalfSheet": "Get hands-free help on the go from Google Assistant", + "assistantSetupNotification": "Tap to set up your Google Assistant", + "fastPairTvConnectDeviceNoAccountDescription": "Connect your %s with this device", + "subsequentPairingDescriptionOnTv": "Connect %s to TV" + } +} diff --git a/fastpair/rust/demo/pubspec.lock b/fastpair/rust/demo/pubspec.lock new file mode 100644 index 00000000..a6087902 --- /dev/null +++ b/fastpair/rust/demo/pubspec.lock @@ -0,0 +1,644 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: "direct main" + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a + url: "https://pub.dev" + source: hosted + version: "8.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: dcb436ba4b466e19da1656ef14622b5ac2ed90efc8fcb0946942c6cd185578d1 + url: "https://pub.dev" + source: hosted + version: "1.79.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + puppeteer: + dependency: transitive + description: + name: puppeteer + sha256: f00b54703dc22af04eaace8f23a33c56008870f990684c2ad8c4115ac51b0a38 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + url: "https://pub.dev" + source: hosted + version: "2.1.1" +sdks: + dart: ">=3.0.6 <4.0.0" diff --git a/fastpair/rust/demo/pubspec.yaml b/fastpair/rust/demo/pubspec.yaml new file mode 100644 index 00000000..3ae724d4 --- /dev/null +++ b/fastpair/rust/demo/pubspec.yaml @@ -0,0 +1,96 @@ +name: demo +description: FLutter UI demo for Fast Pair Windows written in Rust. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.0.6 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + ffi: ^2.0.2 + flutter_rust_bridge: ^1.79.0 + freezed_annotation: ^2.4.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + ffigen: ^8.0.2 + build_runner: ^2.4.6 + freezed: ^2.4.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/fastpair/rust/demo/rust/Cargo.toml b/fastpair/rust/demo/rust/Cargo.toml new file mode 100644 index 00000000..c1839a89 --- /dev/null +++ b/fastpair/rust/demo/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["lib", "cdylib", "staticlib"] + +[dependencies] +anyhow = "1.0" +bluetooth = { version = "0.1", path = "../../bluetooth" } +flutter_rust_bridge = "1" +futures = { version = "0.3", features = ["executor"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1.37" +ttl_cache = "0.5.1" diff --git a/fastpair/rust/demo/rust/src/advertisement.rs b/fastpair/rust/demo/rust/src/advertisement.rs new file mode 100644 index 00000000..bb3dd601 --- /dev/null +++ b/fastpair/rust/demo/rust/src/advertisement.rs @@ -0,0 +1,151 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bluetooth::{BleAddress, BleAdvertisement, ServiceData}; + +use crate::{ + decoder::FpDecoder, + fetcher::{FpFetcher, FpFetcherLocal}, +}; + +/// Represents a FP device model ID. +pub(crate) type ModelId = String; + +/// Holds information required to make decisions about an incoming Fast Pair +/// advertisement. +#[derive(Clone)] +pub(crate) struct FpPairingAdvertisement { + inner: BleAdvertisement, + /// Estimated distance in meters of device from BLE adapter. + distance: f64, + model_id: ModelId, + name: String, + image_url: String, +} + +impl FpPairingAdvertisement { + /// Create a new Fast Pair advertisement instance. + pub(crate) fn new( + adv: BleAdvertisement, + service_data: &ServiceData, + ) -> Result { + let rssi = adv.rssi().ok_or(anyhow::anyhow!( + "Windows advertisements should contain RSSI information." + ))?; + let tx_power = adv.tx_power().ok_or(anyhow::anyhow!( + "Fast Pair advertisements should advertise their transmit power." + ))?; + + let distance = distance_from_rssi_and_tx_power(rssi, tx_power); + + // Extract model ID from service data. We don't need to store service + // data in the `FpPairingAdvertisement` since it's easily accessible from + // `FpPairingAdvertisement.inner`, but it's convenient to save the parsed + // model ID. + let mut model_id = + FpDecoder::get_model_id_from_service_data(service_data).or_else(|err| { + // Some FP advertisements can be GATT non-discoverable + // advertisements containing service data that isn't + // the device model ID. In this case, simply ignore + // advertisements with errors extracting the model ID. + // See: developers.google.com/nearby/fast-pair/specifications/service/provider + Err(anyhow::anyhow!("error extracting model ID: {}", err)) + })?; + + if model_id.len() != 3 { + // In this demo of Fast Pair Rust, only model ID's + // of length 3 bytes are supported. Therefore, if a + // larger model ID makes it this far, log an error. + // TODO b/294453912 + return Err(anyhow::anyhow!("Error: model ID of unsupported length")); + } + + // Pad with 0 at the beginning to successfully call `from_be_bytes`. + // Assumes `model_id.len() == 3` before the call to `insert`, otherwise + // this will panic. + model_id.insert(0, 0); + let model_id = format!("{}", u32::from_be_bytes(model_id.try_into().unwrap())); + + // Retrieve device info of the device corresponding to this model ID. + let fetcher = FpFetcherLocal::new(String::from("./local")); + let device_info = fetcher + .get_device_info_from_model_id(&model_id) + .expect("Failed to create device info from model ID."); + + Ok(FpPairingAdvertisement { + inner: adv, + distance, + model_id, + name: device_info.name().to_string(), + image_url: device_info.image_url().to_string(), + }) + } + + /// Retrieve estimated distance the BLE advertisement travelled between + /// the sending device and this receiver. + pub(crate) fn distance(&self) -> f64 { + self.distance + } + + /// Retrieve the BLE Address of the advertising device. + pub(crate) fn address(&self) -> BleAddress { + self.inner.address() + } + + /// Retrieve the Model ID advertised by this device, parsed from the + /// 16-bit UUID service data. + pub(crate) fn model_id(&self) -> &ModelId { + &self.model_id + } + + pub(crate) fn name(&self) -> &String { + &self.name + } + + pub(crate) fn image_url(&self) -> &String { + &self.image_url + } +} + +/// Convert RSSI and transmit power to distance using log-distance path loss +/// model, with reference path loss of 1m at 41dB in free space. +/// See: https://en.wikipedia.org/wiki/Log-distance_path_loss_model. +#[inline] +pub(crate) fn distance_from_rssi_and_tx_power(rssi: i16, tx_power: i16) -> f64 { + // Source: Android Nearby implementation, `RangingUtils.java`. + // + // PL = total path loss in db + // txPower = TxPower in dbm + // rssi = Received signal strength in dbm + // PL_0 = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm + // d = length of path + // d_0 = reference distance (1 m) + // gamma = path loss exponent (2 in free space) + // + // Log-distance path loss (LDPL) formula: + // + // PL = txPower - rssi = PL_0 + 10 * gamma * log_10(d / d_0) + // txPower - rssi = RSSI_DROP_OFF_AT_1_M + 10 * gamma * log_10(d / d_0) + // txPower - rssi - RSSI_DROP_OFF_AT_1_M = 10 * 2 * log_10(distanceInMeters / 1) + // txPower - rssi - RSSI_DROP_OFF_AT_1_M = 20 * log_10(distanceInMeters / 1) + // (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20 = log_10(distanceInMeters) + // 10 ^ ((txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20) = distanceInMeters + + const RSSI_DROPOFF_AT_1_M: i16 = 41; + const PATH_LOSS_EXPONENT: i16 = 2; + + f64::from(10.0).powf( + (f64::from(tx_power - rssi - RSSI_DROPOFF_AT_1_M)) / f64::from(10 * PATH_LOSS_EXPONENT), + ) +} diff --git a/fastpair/rust/demo/rust/src/api.rs b/fastpair/rust/demo/rust/src/api.rs new file mode 100644 index 00000000..e80ae950 --- /dev/null +++ b/fastpair/rust/demo/rust/src/api.rs @@ -0,0 +1,222 @@ +use std::{collections::HashMap, sync::RwLock, time::Duration}; + +use bluetooth::{ + api::{BleAdapter, ClassicDevice}, + BleAdvertisement, BleDataTypeId, ClassicAddress, PairingResult, Platform, ServiceData, +}; +use flutter_rust_bridge::StreamSink; +use futures::executor; +use tracing::{info, warn}; +use ttl_cache::TtlCache; + +use crate::advertisement::{FpPairingAdvertisement, ModelId}; + +// Sends a device name to Flutter via `StreamSink` FFI layer. +static DEVICE_STREAM: RwLock>>> = RwLock::new(None); + +// Saves the currently displayed device's advertisement, to be used for pairing. +static CURR_DEVICE_ADV: RwLock> = RwLock::new(None); + +// Temporarily restricts which model IDs can be displayed. +static MODEL_ID_BLACKLIST: RwLock>> = RwLock::new(None); + +// How long entries should blacklisted for for. +const TTL_BLACKLIST: Duration = Duration::from_secs(10); + +/// Updates the device name as displayed by Flutter. +#[inline] +async fn update_best_device(best_adv: FpPairingAdvertisement) { + match DEVICE_STREAM.read().unwrap().as_ref() { + Some(stream) => { + stream.add(Some([ + best_adv.name().to_string(), + best_adv.image_url().to_string(), + ])); + } + None => info!("Name stream is None"), + } + let mut curr_adv = CURR_DEVICE_ADV.write().unwrap(); + *curr_adv = Some(best_adv); +} + +/// Determines whether the device advertised by the provided service data is the +/// closest Fast Pair device. +/// If this device has been seen previously but has now moved further away, +/// decide which other seen device is now closer. +#[inline] +fn new_best_fp_advertisement( + advertisement: BleAdvertisement, + service_data: &ServiceData, + latest_advertisement_map: &mut HashMap, +) -> Option { + // Analyze service data sections. + let uuid = service_data.uuid(); + + // This is not a Fast Pair device. + if uuid != 0x2cfe { + return None; + } + + let fp_adv = match FpPairingAdvertisement::new(advertisement, service_data) { + Ok(fp_adv) => fp_adv, + Err(err) => { + // If error during construction (e.g. non-discoverable + // Fast Pair device not advertising tx_power or + // with service data that isn't model ID) ignore + // this advertisement section. + warn!("Error creating FP Advertisement: {}", err); + return None; + } + }; + + // If blacklisted in TTL cache, skip this advertisement. + let blacklisted = match MODEL_ID_BLACKLIST.read().unwrap().as_ref() { + Some(cache) => cache.get(fp_adv.model_id()).is_some(), + None => false, + }; + if blacklisted { + latest_advertisement_map.remove(fp_adv.model_id()); + return None; + } + + latest_advertisement_map.insert(fp_adv.model_id().to_owned(), fp_adv.clone()); + + if let Some(best_adv) = CURR_DEVICE_ADV.read().unwrap().as_ref() { + if best_adv.distance() >= fp_adv.distance() { + // New advertised distance is closer. + Some(fp_adv) + } else if best_adv.model_id() == fp_adv.model_id() { + // New advertised distance by the previous best device has + // increased, so select new closest device. + let next_best_adv_ref = + latest_advertisement_map + .values() + .into_iter() + .min_by(|adv1, adv2| { + // We should never get NaN, so it's okay to unwrap. + adv1.distance().partial_cmp(&adv2.distance()).unwrap() + }); + + if let Some(next_best_adv) = next_best_adv_ref { + Some(next_best_adv.to_owned()) + } else { + None + } + } else { + None + } + } else { + // First discovered device, must be closest. + Some(fp_adv) + } +} + +/// Sets up necessary constructs to maintain a TTL blacklist of model IDs. +#[inline] +fn init_cache() { + let mut cache = MODEL_ID_BLACKLIST.write().unwrap(); + *cache = Some(TtlCache::new(16)); +} + +/// Sets up initial constructs and infinitely polls for advertisements. +pub fn init() { + let run = async { + info!("start making adapter"); + + let mut adapter = Platform::default_adapter().await.unwrap(); + adapter.start_scan().unwrap(); + + init_cache(); + + let mut latest_advertisement_map = HashMap::new(); + let datatype_selector = vec![BleDataTypeId::ServiceData16BitUuid]; + + loop { + // Retrieve the next received advertisement. + let advertisement = adapter + .next_advertisement(Some(&datatype_selector)) + .await + .unwrap(); + + for service_data in advertisement.service_data_16bit_uuid().unwrap() { + if let Some(best_adv) = new_best_fp_advertisement( + advertisement.clone(), + service_data, + &mut latest_advertisement_map, + ) { + update_best_device(best_adv).await; + } + } + } + }; + + executor::block_on(run) +} + +/// Sets up `StreamSink` for Dart-Rust FFI. +pub fn event_stream(s: StreamSink>) -> Result<(), anyhow::Error> { + let mut stream = DEVICE_STREAM.write().unwrap(); + *stream = Some(s); + Ok(()) +} + +/// Attempt classic pairing with currently displayed device. +pub fn pair() -> String { + let result = match CURR_DEVICE_ADV.read().unwrap().as_ref() { + Some(adv) => { + let run = async { + let classic_addr = ClassicAddress::try_from(adv.address()).unwrap(); + let classic_device = Platform::new_classic_device(classic_addr).await.unwrap(); + + match classic_device.pair().await { + Ok(result) => match result { + PairingResult::Success => String::from("Pairing success!"), + PairingResult::AlreadyPaired => { + String::from("This device is already paired.") + } + PairingResult::AlreadyInProgress => { + String::from("Pairing already in progress.") + } + _ => String::from("Unknown result."), + }, + Err(err) => { + format!("Error {}", err) + } + } + }; + + executor::block_on(run) + } + None => String::from("No device available to pair."), + }; + info!(result); + result +} + +/// Remove this device from display and add it to the TTL cache blacklist. +pub fn dismiss() { + let run = async { + let mut adv = CURR_DEVICE_ADV.write().unwrap(); + match MODEL_ID_BLACKLIST.write().unwrap().as_mut() { + Some(cache) => { + let adv = adv.take(); + match adv { + Some(adv) => { + cache.insert(adv.model_id().to_string(), (), TTL_BLACKLIST); + } + None => (), + } + + match DEVICE_STREAM.read().unwrap().as_ref() { + Some(stream) => { + stream.add(None); + } + None => (), + } + } + None => (), + } + }; + + executor::block_on(run); +} diff --git a/fastpair/rust/demo/rust/src/bridge_generated.io.rs b/fastpair/rust/demo/rust/src/bridge_generated.io.rs new file mode 100644 index 00000000..cb0a62f4 --- /dev/null +++ b/fastpair/rust/demo/rust/src/bridge_generated.io.rs @@ -0,0 +1,65 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +// Section: wire functions + +#[no_mangle] +pub extern "C" fn wire_init(port_: i64) { + wire_init_impl(port_) +} + +#[no_mangle] +pub extern "C" fn wire_event_stream(port_: i64) { + wire_event_stream_impl(port_) +} + +#[no_mangle] +pub extern "C" fn wire_pair(port_: i64) { + wire_pair_impl(port_) +} + +#[no_mangle] +pub extern "C" fn wire_dismiss(port_: i64) { + wire_dismiss_impl(port_) +} + +// Section: allocate functions + +// Section: related functions + +// Section: impl Wire2Api + +// Section: wire structs + +// Section: impl NewWithNullPtr + +pub trait NewWithNullPtr { + fn new_with_null_ptr() -> Self; +} + +impl NewWithNullPtr for *mut T { + fn new_with_null_ptr() -> Self { + std::ptr::null_mut() + } +} + +// Section: sync execution mode utility + +#[no_mangle] +pub extern "C" fn free_WireSyncReturn(ptr: support::WireSyncReturn) { + unsafe { + let _ = support::box_from_leak_ptr(ptr); + }; +} diff --git a/fastpair/rust/demo/rust/src/bridge_generated.rs b/fastpair/rust/demo/rust/src/bridge_generated.rs new file mode 100644 index 00000000..06d8ce6e --- /dev/null +++ b/fastpair/rust/demo/rust/src/bridge_generated.rs @@ -0,0 +1,115 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow( + non_camel_case_types, + unused, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::double_parens, + non_snake_case, + clippy::too_many_arguments +)] +// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`@ 1.79.0. + +use crate::api::*; +use core::panic::UnwindSafe; +use flutter_rust_bridge::rust2dart::IntoIntoDart; +use flutter_rust_bridge::*; +use std::ffi::c_void; +use std::sync::Arc; + +// Section: imports + +// Section: wire functions + +fn wire_init_impl(port_: MessagePort) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, ()>( + WrapInfo { + debug_name: "init", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || move |task_callback| Ok(init()), + ) +} +fn wire_event_stream_impl(port_: MessagePort) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, ()>( + WrapInfo { + debug_name: "event_stream", + port: Some(port_), + mode: FfiCallMode::Stream, + }, + move || { + move |task_callback| event_stream(task_callback.stream_sink::<_, Option<[String; 2]>>()) + }, + ) +} +fn wire_pair_impl(port_: MessagePort) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, String>( + WrapInfo { + debug_name: "pair", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || move |task_callback| Ok(pair()), + ) +} +fn wire_dismiss_impl(port_: MessagePort) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, ()>( + WrapInfo { + debug_name: "dismiss", + port: Some(port_), + mode: FfiCallMode::Normal, + }, + move || move |task_callback| Ok(dismiss()), + ) +} +// Section: wrapper structs + +// Section: static checks + +// Section: allocate functions + +// Section: related functions + +// Section: impl Wire2Api + +pub trait Wire2Api { + fn wire2api(self) -> T; +} + +impl Wire2Api> for *mut S +where + *mut S: Wire2Api, +{ + fn wire2api(self) -> Option { + (!self.is_null()).then(|| self.wire2api()) + } +} +// Section: impl IntoDart + +// Section: executor + +support::lazy_static! { + pub static ref FLUTTER_RUST_BRIDGE_HANDLER: support::DefaultHandler = Default::default(); +} + +#[cfg(not(target_family = "wasm"))] +#[path = "bridge_generated.io.rs"] +mod io; +#[cfg(not(target_family = "wasm"))] +pub use io::*; diff --git a/fastpair/rust/demo/rust/src/decoder.rs b/fastpair/rust/demo/rust/src/decoder.rs new file mode 100644 index 00000000..dd0f80d4 --- /dev/null +++ b/fastpair/rust/demo/rust/src/decoder.rs @@ -0,0 +1,49 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use bluetooth::ServiceData; + +/// Unit struct providing parsing operations for Fast Pair advertisements. +pub(crate) struct FpDecoder; + +impl FpDecoder { + /// Retrieve the Fast Pair device model ID from a service data payload. + /// https://developers.google.com/nearby/fast-pair/specifications/service/provider. + /// * Length < 3: invalid payload + /// * Length == 3: entire payload is the model ID + /// * Length > 3: first byte specifies the length of the model ID, in bytes. + /// Currently unavailable in Fast Pair devices and not supported. + pub(crate) fn get_model_id_from_service_data( + service_data: &ServiceData, + ) -> Result, anyhow::Error> { + static MIN_MODEL_ID_LENGTH: usize = 3; + let data = service_data.data(); + + if data.len() < MIN_MODEL_ID_LENGTH { + // If service data too small, invalid payload. + Err(anyhow::anyhow!(format!( + "Invalid model ID for Fast Pair advertisement of length {}.", + data.len() + ))) + } else if data.len() == MIN_MODEL_ID_LENGTH { + // Else if service data length is exactly 3, all bytes are the ID. + Ok(data.clone()) + } else { + // Else, this Fast Pair advertisement is currently unsupported. + // b/294453912 + Err(anyhow::anyhow!( + "This Fast Pair device is currently unsupported." + )) + } + } +} diff --git a/fastpair/rust/demo/rust/src/fetcher.rs b/fastpair/rust/demo/rust/src/fetcher.rs new file mode 100644 index 00000000..e94d49ad --- /dev/null +++ b/fastpair/rust/demo/rust/src/fetcher.rs @@ -0,0 +1,81 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::fs; + +use serde::Deserialize; + +use crate::advertisement::ModelId; + +/// Holds Fast Pair device information parsed from JSON. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct DeviceInfo { + image_url: String, + name: String, +} + +/// Holds top-level Fast Pair information parsed from JSON. See `local` +/// directory for format. +#[derive(Deserialize)] +struct JsonData { + device: DeviceInfo, +} + +/// Types that can fetch Fast Pair data from external storage (e.g. filesystem, +/// remote server). +pub(crate) trait FpFetcher { + fn get_device_info_from_model_id( + &self, + model_id: &ModelId, + ) -> Result; +} + +/// A unit struct for retrieving Fast Pair information from the local filesystem. +pub(crate) struct FpFetcherLocal { + path: String, +} + +impl FpFetcherLocal { + pub(crate) fn new(path: String) -> Self { + FpFetcherLocal { path } + } +} + +impl FpFetcher for FpFetcherLocal { + /// Retrieve device information for the provided Model ID. Currently, + /// this information is saved locally. In the future, this should instead + /// be retrieved from a remote server and cached. + /// b/294456411 + fn get_device_info_from_model_id( + &self, + model_id: &ModelId, + ) -> Result { + let file_path = format!("{}/{}.json", self.path, model_id); + let contents = fs::read_to_string(file_path).expect("Couldn't find or open file."); + + let model_info: JsonData = serde_json::from_str(&contents)?; + + Ok(model_info.device) + } +} + +impl DeviceInfo { + pub(crate) fn name(&self) -> &String { + &self.name + } + + pub(crate) fn image_url(&self) -> &String { + &self.image_url + } +} diff --git a/fastpair/rust/demo/rust/src/lib.rs b/fastpair/rust/demo/rust/src/lib.rs new file mode 100644 index 00000000..34ba9081 --- /dev/null +++ b/fastpair/rust/demo/rust/src/lib.rs @@ -0,0 +1,19 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod advertisement; +mod api; +mod bridge_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */ +mod decoder; +mod fetcher; diff --git a/fastpair/rust/demo/windows/.gitignore b/fastpair/rust/demo/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/fastpair/rust/demo/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/fastpair/rust/demo/windows/CMakeLists.txt b/fastpair/rust/demo/windows/CMakeLists.txt new file mode 100644 index 00000000..172a9862 --- /dev/null +++ b/fastpair/rust/demo/windows/CMakeLists.txt @@ -0,0 +1,103 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(demo LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "demo") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) +include(./rust.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/fastpair/rust/demo/windows/flutter/CMakeLists.txt b/fastpair/rust/demo/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/fastpair/rust/demo/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/fastpair/rust/src/lib.rs b/fastpair/rust/demo/windows/flutter/generated_plugin_registrant.cc similarity index 78% rename from fastpair/rust/src/lib.rs rename to fastpair/rust/demo/windows/flutter/generated_plugin_registrant.cc index c9a294f5..5da9cfa4 100644 --- a/fastpair/rust/src/lib.rs +++ b/fastpair/rust/demo/windows/flutter/generated_plugin_registrant.cc @@ -12,5 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// Library file, exports modules for use in integration tests and external crates. -pub mod bluetooth; +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "./generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/fastpair/rust/demo/windows/flutter/generated_plugin_registrant.h b/fastpair/rust/demo/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..7e32ef7b --- /dev/null +++ b/fastpair/rust/demo/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/fastpair/rust/demo/windows/flutter/generated_plugins.cmake b/fastpair/rust/demo/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..b93c4c30 --- /dev/null +++ b/fastpair/rust/demo/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/fastpair/rust/demo/windows/runner/CMakeLists.txt b/fastpair/rust/demo/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/fastpair/rust/demo/windows/runner/Runner.rc b/fastpair/rust/demo/windows/runner/Runner.rc new file mode 100644 index 00000000..3c7cdc73 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "demo" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "demo" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "demo.exe" "\0" + VALUE "ProductName", "demo" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/fastpair/rust/demo/windows/runner/flutter_window.cpp b/fastpair/rust/demo/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..0f6bf6e4 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/flutter_window.cpp @@ -0,0 +1,80 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/fastpair/rust/demo/windows/runner/flutter_window.h b/fastpair/rust/demo/windows/runner/flutter_window.h new file mode 100644 index 00000000..adb318a0 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/flutter_window.h @@ -0,0 +1,47 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "./win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/fastpair/rust/demo/windows/runner/main.cpp b/fastpair/rust/demo/windows/runner/main.cpp new file mode 100644 index 00000000..2827bb3a --- /dev/null +++ b/fastpair/rust/demo/windows/runner/main.cpp @@ -0,0 +1,57 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "./flutter_window.h" +#include "./utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"demo", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/fastpair/rust/demo/windows/runner/resource.h b/fastpair/rust/demo/windows/runner/resource.h new file mode 100644 index 00000000..85a4c6eb --- /dev/null +++ b/fastpair/rust/demo/windows/runner/resource.h @@ -0,0 +1,33 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// {{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#ifndef RUNNER_RESOURCE_H_ +#define RUNNER_RESOURCE_H_ +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif +#endif // RUNNER_RESOURCE_H_ diff --git a/fastpair/rust/demo/windows/runner/resources/app_icon.ico b/fastpair/rust/demo/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/fastpair/rust/demo/windows/runner/resources/app_icon.ico differ diff --git a/fastpair/rust/demo/windows/runner/runner.exe.manifest b/fastpair/rust/demo/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/fastpair/rust/demo/windows/runner/utils.cpp b/fastpair/rust/demo/windows/runner/utils.cpp new file mode 100644 index 00000000..070097ec --- /dev/null +++ b/fastpair/rust/demo/windows/runner/utils.cpp @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/fastpair/rust/demo/windows/runner/utils.h b/fastpair/rust/demo/windows/runner/utils.h new file mode 100644 index 00000000..b089ab5f --- /dev/null +++ b/fastpair/rust/demo/windows/runner/utils.h @@ -0,0 +1,33 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/fastpair/rust/demo/windows/runner/win32_window.cpp b/fastpair/rust/demo/windows/runner/win32_window.cpp new file mode 100644 index 00000000..b3c2fa35 --- /dev/null +++ b/fastpair/rust/demo/windows/runner/win32_window.cpp @@ -0,0 +1,303 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./win32_window.h" + +#include +#include + +#include "./resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = + L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/fastpair/rust/demo/windows/runner/win32_window.h b/fastpair/rust/demo/windows/runner/win32_window.h new file mode 100644 index 00000000..0597809c --- /dev/null +++ b/fastpair/rust/demo/windows/runner/win32_window.h @@ -0,0 +1,116 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/fastpair/rust/demo/windows/rust.cmake b/fastpair/rust/demo/windows/rust.cmake new file mode 100644 index 00000000..4931b359 --- /dev/null +++ b/fastpair/rust/demo/windows/rust.cmake @@ -0,0 +1,21 @@ +# We include Corrosion inline here, but ideally in a project with +# many dependencies we would need to install Corrosion on the system. +# See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install +# Once done, uncomment this line: +# find_package(Corrosion REQUIRED) + +include(FetchContent) + +FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git + GIT_TAG origin/master # Optionally specify a version tag or branch here +) + +FetchContent_MakeAvailable(Corrosion) + +corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml IMPORTED_CRATES imported_crates) +target_link_libraries(${BINARY_NAME} PRIVATE ${imported_crates}) +foreach(imported_crate ${imported_crates}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) +endforeach() \ No newline at end of file