mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 22:56:12 -04:00
[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:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user