[fp-rs] Added device dismissing functionality. The Seeker will ignore a dismissed device's advertisements until a 10s timeout expires.

This commit is contained in:
Lucas Silva Shepard
2023-08-14 13:33:46 -07:00
parent 57373a80c3
commit a683ed6789
7 changed files with 208 additions and 78 deletions
@@ -17,7 +17,7 @@ abstract class Rust {
FlutterRustBridgeTaskConstMeta get kInitConstMeta;
/// Sets up `StreamSink` for Dart-Rust FFI.
Stream<StringArray2> eventStream({dynamic hint});
Stream<StringArray2?> eventStream({dynamic hint});
FlutterRustBridgeTaskConstMeta get kEventStreamConstMeta;
@@ -25,6 +25,10 @@ abstract class Rust {
Future<String> pair({dynamic hint});
FlutterRustBridgeTaskConstMeta get kPairConstMeta;
Future<void> dismiss({dynamic hint});
FlutterRustBridgeTaskConstMeta get kDismissConstMeta;
}
class StringArray2 extends NonGrowableListView<String> {
+34 -2
View File
@@ -41,10 +41,10 @@ class RustImpl implements Rust {
argNames: [],
);
Stream<StringArray2> eventStream({dynamic hint}) {
Stream<StringArray2?> 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<void> 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<dynamic>).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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_pair');
late final _wire_pair = _wire_pairPtr.asFunction<void Function(int)>();
void wire_dismiss(
int port_,
) {
return _wire_dismiss(
port_,
);
}
late final _wire_dismissPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_dismiss');
late final _wire_dismiss = _wire_dismissPtr.asFunction<void Function(int)>();
void free_WireSyncReturn(
WireSyncReturn ptr,
) {
+89 -66
View File
@@ -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<String>(
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: <Widget>[
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: <Widget>[
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<String?> pairing(BuildContext context) => showDialog<String>(
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: <Widget>[
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: <Widget>[
SizedBox(
width: 50,
height: 50,
child: CircularProgressIndicator(),
),
],
),
);
}));
+1
View File
@@ -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"
+61 -8
View File
@@ -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<Option<StreamSink<[String; 2]>>> = RwLock::new(None);
static DEVICE_STREAM: RwLock<Option<StreamSink<Option<[String; 2]>>>> = RwLock::new(None);
// Saves the currently displayed device's advertisement, to be used for pairing.
static CURR_DEVICE_ADV: RwLock<Option<FpPairingAdvertisement>> = RwLock::new(None);
// Temporarily restricts which model IDs can be displayed.
static MODEL_ID_BLACKLIST: RwLock<Option<TtlCache<ModelId, ()>>> = 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<Option<[String; 2]>>) -> 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);
}
@@ -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
@@ -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