diff --git a/fastpair/rust/demo/lib/bridge_definitions.dart b/fastpair/rust/demo/lib/bridge_definitions.dart index 5e44a0de..9c6d91bb 100644 --- a/fastpair/rust/demo/lib/bridge_definitions.dart +++ b/fastpair/rust/demo/lib/bridge_definitions.dart @@ -17,7 +17,7 @@ abstract class Rust { FlutterRustBridgeTaskConstMeta get kInitConstMeta; /// Sets up `StreamSink` for Dart-Rust FFI. - Stream eventStream({dynamic hint}); + Stream eventStream({dynamic hint}); FlutterRustBridgeTaskConstMeta get kEventStreamConstMeta; @@ -25,6 +25,10 @@ abstract class Rust { Future pair({dynamic hint}); FlutterRustBridgeTaskConstMeta get kPairConstMeta; + + Future dismiss({dynamic hint}); + + FlutterRustBridgeTaskConstMeta get kDismissConstMeta; } class StringArray2 extends NonGrowableListView { diff --git a/fastpair/rust/demo/lib/bridge_generated.dart b/fastpair/rust/demo/lib/bridge_generated.dart index 77557675..9fc5d9db 100644 --- a/fastpair/rust/demo/lib/bridge_generated.dart +++ b/fastpair/rust/demo/lib/bridge_generated.dart @@ -41,10 +41,10 @@ class RustImpl implements Rust { argNames: [], ); - Stream eventStream({dynamic hint}) { + Stream eventStream({dynamic hint}) { return _platform.executeStream(FlutterRustBridgeTask( callFfi: (port_) => _platform.inner.wire_event_stream(port_), - parseSuccessData: _wire2api_String_array_2, + parseSuccessData: _wire2api_opt_String_array_2, constMeta: kEventStreamConstMeta, argValues: [], hint: hint, @@ -73,6 +73,22 @@ class RustImpl implements Rust { 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(); } @@ -90,6 +106,10 @@ class RustImpl implements Rust { 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; } @@ -250,6 +270,18 @@ class RustWire implements FlutterRustBridgeWireBase { _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, ) { diff --git a/fastpair/rust/demo/lib/main.dart b/fastpair/rust/demo/lib/main.dart index 40534f31..ca2565e4 100644 --- a/fastpair/rust/demo/lib/main.dart +++ b/fastpair/rust/demo/lib/main.dart @@ -8,7 +8,6 @@ void main() { class FastPairApp extends StatelessWidget { const FastPairApp({super.key}); - @override Widget build(BuildContext context) => MaterialApp( title: 'Fast Pair', @@ -24,70 +23,94 @@ class HomePage extends StatelessWidget { @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) { - if (deviceInfo.hasData) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // `deviceInfo.data[0]` holds device name. - // `deviceInfo.data[1]` holds image URL. - Expanded( - child: Image.network(deviceInfo.data![1], - fit: BoxFit.contain)), - Text(deviceInfo.data![0]), - OutlinedButton( - // Invoke pairing dialog. - onPressed: () => showDialog( - context: context, - // Rust functions are invoked as futures. - builder: (context) => FutureBuilder( - future: api.pair(), - builder: (context, pairResult) { - return pairResult.hasData - ? AlertDialog( - title: const Text('Pairing result'), - content: Text(pairResult.data!), - 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(), - ), - ], - ), - ); - })), - child: const Text('Pair'), - ), - ]); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, + 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/rust/Cargo.toml b/fastpair/rust/demo/rust/Cargo.toml index 23463338..c1839a89 100644 --- a/fastpair/rust/demo/rust/Cargo.toml +++ b/fastpair/rust/demo/rust/Cargo.toml @@ -16,3 +16,4 @@ 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/api.rs b/fastpair/rust/demo/rust/src/api.rs index 8f06d78c..e80ae950 100644 --- a/fastpair/rust/demo/rust/src/api.rs +++ b/fastpair/rust/demo/rust/src/api.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::RwLock}; +use std::{collections::HashMap, sync::RwLock, time::Duration}; use bluetooth::{ api::{BleAdapter, ClassicDevice}, @@ -7,24 +7,31 @@ use bluetooth::{ use flutter_rust_bridge::StreamSink; use futures::executor; use tracing::{info, warn}; +use ttl_cache::TtlCache; -use crate::advertisement::FpPairingAdvertisement; +use crate::advertisement::{FpPairingAdvertisement, ModelId}; // Sends a device name to Flutter via `StreamSink` FFI layer. -static DEVICE_STREAM: RwLock>> = RwLock::new(None); +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([ + stream.add(Some([ best_adv.name().to_string(), best_adv.image_url().to_string(), - ]); + ])); } None => info!("Name stream is None"), } @@ -62,6 +69,16 @@ fn new_best_fp_advertisement( } }; + // 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() { @@ -94,6 +111,13 @@ fn new_best_fp_advertisement( } } +/// 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 { @@ -102,6 +126,8 @@ pub fn init() { 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]; @@ -128,19 +154,18 @@ pub fn init() { } /// Sets up `StreamSink` for Dart-Rust FFI. -pub fn event_stream(s: StreamSink<[String; 2]>) -> Result<(), anyhow::Error> { +pub fn event_stream(s: StreamSink>) -> Result<(), anyhow::Error> { let mut stream = DEVICE_STREAM.write().unwrap(); *stream = Some(s); Ok(()) } -/// Attempt classic pairing with device of address `CURR_ADDRESS`. +/// 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 { @@ -167,3 +192,31 @@ pub fn pair() -> String { 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 index a3892eb9..12296aa8 100644 --- a/fastpair/rust/demo/rust/src/bridge_generated.io.rs +++ b/fastpair/rust/demo/rust/src/bridge_generated.io.rs @@ -16,6 +16,11 @@ 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 diff --git a/fastpair/rust/demo/rust/src/bridge_generated.rs b/fastpair/rust/demo/rust/src/bridge_generated.rs index 5d1523c6..8a2429f4 100644 --- a/fastpair/rust/demo/rust/src/bridge_generated.rs +++ b/fastpair/rust/demo/rust/src/bridge_generated.rs @@ -39,7 +39,9 @@ fn wire_event_stream_impl(port_: MessagePort) { port: Some(port_), mode: FfiCallMode::Stream, }, - move || move |task_callback| event_stream(task_callback.stream_sink::<_, [String; 2]>()), + move || { + move |task_callback| event_stream(task_callback.stream_sink::<_, Option<[String; 2]>>()) + }, ) } fn wire_pair_impl(port_: MessagePort) { @@ -52,6 +54,16 @@ fn wire_pair_impl(port_: MessagePort) { 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