1 //! Suspend/Resume API.
2
3 use crate::bluetooth::{
4 AdapterActions, Bluetooth, BluetoothDevice, BtifBluetoothCallbacks, IBluetooth,
5 IBluetoothConnectionCallback,
6 };
7 use crate::bluetooth_media::BluetoothMedia;
8 use crate::callbacks::Callbacks;
9 use crate::{BluetoothGatt, Message, RPCProxy};
10 use bt_topshim::btif::{BluetoothInterface, BtStatus, RawAddress};
11 use bt_topshim::metrics;
12 use log::warn;
13 use num_derive::{FromPrimitive, ToPrimitive};
14 use std::collections::HashSet;
15 use std::iter::FromIterator;
16 use std::sync::{Arc, Mutex, MutexGuard};
17 use tokio::sync::mpsc::Sender;
18 use tokio::task::JoinHandle;
19 use tokio::time;
20 use tokio::time::Duration;
21
22 use bt_utils::socket::{BtSocket, HciChannels, MgmtCommand, HCI_DEV_NONE};
23
24 /// Defines the Suspend/Resume API.
25 ///
26 /// This API is exposed by `btadapterd` and independent of the suspend/resume detection mechanism
27 /// which depends on the actual operating system the daemon runs on. Possible clients of this API
28 /// include `btmanagerd` with Chrome OS `powerd` integration, `btmanagerd` with systemd Inhibitor
29 /// interface, or any script hooked to suspend/resume events.
30 pub trait ISuspend {
31 /// Adds an observer to suspend events.
32 ///
33 /// Returns true if the callback can be registered.
register_callback(&mut self, callback: Box<dyn ISuspendCallback + Send>) -> bool34 fn register_callback(&mut self, callback: Box<dyn ISuspendCallback + Send>) -> bool;
35
36 /// Removes an observer to suspend events.
37 ///
38 /// Returns true if the callback can be removed, false if `callback_id` is not recognized.
unregister_callback(&mut self, callback_id: u32) -> bool39 fn unregister_callback(&mut self, callback_id: u32) -> bool;
40
41 /// Prepares the stack for suspend, identified by `suspend_id`.
42 ///
43 /// Returns a positive number identifying the suspend if it can be started. If there is already
44 /// a suspend, that active suspend id is returned.
suspend(&mut self, suspend_type: SuspendType, suspend_id: i32)45 fn suspend(&mut self, suspend_type: SuspendType, suspend_id: i32);
46
47 /// Undoes previous suspend preparation identified by `suspend_id`.
48 ///
49 /// Returns true if suspend can be resumed, and false if there is no suspend to resume.
resume(&mut self) -> bool50 fn resume(&mut self) -> bool;
51 }
52
53 /// Suspend events.
54 pub trait ISuspendCallback: RPCProxy {
55 /// Triggered when a callback is registered and given an identifier `callback_id`.
on_callback_registered(&mut self, callback_id: u32)56 fn on_callback_registered(&mut self, callback_id: u32);
57
58 /// Triggered when the stack is ready for suspend and tell the observer the id of the suspend.
on_suspend_ready(&mut self, suspend_id: i32)59 fn on_suspend_ready(&mut self, suspend_id: i32);
60
61 /// Triggered when the stack has resumed the previous suspend.
on_resumed(&mut self, suspend_id: i32)62 fn on_resumed(&mut self, suspend_id: i32);
63 }
64
65 /// When we resume, we will want to reconnect audio devices that were previously connected.
66 /// However, we will need to delay a few seconds to avoid co-ex issues with Wi-Fi reconnection.
67 const RECONNECT_AUDIO_ON_RESUME_DELAY_MS: u64 = 3000;
68
69 /// Delay sending suspend ready signal by some time because HCI commands are async and we could
70 /// still receive some commands/events after all LibBluetooth functions have returned.
71 const SUSPEND_READY_DELAY_MS: u64 = 100;
72
notify_suspend_state(hci_index: u16, suspended: bool)73 fn notify_suspend_state(hci_index: u16, suspended: bool) {
74 log::debug!("Notify kernel suspend status: {} for hci{}", suspended, hci_index);
75 let mut btsock = BtSocket::new();
76 match btsock.open() {
77 -1 => {
78 panic!(
79 "Bluetooth socket unavailable (errno {}). Try loading the kernel module first.",
80 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
81 );
82 }
83 x => log::debug!("notify suspend Socket open at fd: {}", x),
84 }
85 // Bind to control channel (which is used for mgmt commands). We provide
86 // HCI_DEV_NONE because we don't actually need a valid HCI dev for some MGMT commands.
87 match btsock.bind_channel(HciChannels::Control, HCI_DEV_NONE) {
88 -1 => {
89 panic!(
90 "Failed to bind control channel with errno={}",
91 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
92 );
93 }
94 _ => (),
95 };
96
97 let command = MgmtCommand::FlossNotifySuspendState(hci_index, suspended);
98 let bytes_written = btsock.write_mgmt_packet(command.into());
99 if bytes_written <= 0 {
100 log::error!("Failed to notify suspend state on hci:{} to {}", hci_index, suspended);
101 }
102 }
103
104 pub enum SuspendActions {
105 CallbackRegistered(u32),
106 CallbackDisconnected(u32),
107 SuspendReady(i32),
108 ResumeReady(i32),
109 AudioReconnectOnResumeComplete,
110 DeviceDisconnected(RawAddress),
111 }
112
113 #[derive(Debug, FromPrimitive, ToPrimitive)]
114 #[repr(u32)]
115 pub enum SuspendType {
116 NoWakesAllowed,
117 AllowWakeFromHid,
118 Other,
119 }
120
121 struct SuspendState {
122 suspend_expected: bool,
123 suspend_id: Option<i32>,
124 wake_allowed: bool,
125
126 disconnect_expected: HashSet<RawAddress>,
127 disconnect_timeout_timer: Option<JoinHandle<()>>,
128
129 delay_timer: Option<JoinHandle<()>>,
130 }
131
132 impl SuspendState {
new() -> SuspendState133 fn new() -> SuspendState {
134 Self {
135 suspend_expected: false,
136 suspend_id: None,
137 wake_allowed: false,
138 disconnect_expected: HashSet::default(),
139 disconnect_timeout_timer: None,
140 delay_timer: None,
141 }
142 }
143
144 // The tasks should remove their timer when they are done, so if all are None then we're ready.
ready_to_suspend(&self) -> bool145 fn ready_to_suspend(&self) -> bool {
146 self.delay_timer.is_none() && self.disconnect_timeout_timer.is_none()
147 }
148 }
149
150 /// Implementation of the suspend API.
151 pub struct Suspend {
152 bt: Arc<Mutex<Box<Bluetooth>>>,
153 intf: Arc<Mutex<BluetoothInterface>>,
154 gatt: Arc<Mutex<Box<BluetoothGatt>>>,
155 media: Arc<Mutex<Box<BluetoothMedia>>>,
156 tx: Sender<Message>,
157 callbacks: Callbacks<dyn ISuspendCallback + Send>,
158
159 /// This list keeps track of audio devices that had an audio profile before
160 /// suspend so that we can attempt to connect after suspend.
161 audio_reconnect_list: Vec<BluetoothDevice>,
162
163 /// Active reconnection attempt after resume.
164 audio_reconnect_joinhandle: Option<JoinHandle<()>>,
165
166 suspend_state: Arc<Mutex<SuspendState>>,
167 }
168
169 impl Suspend {
new( bt: Arc<Mutex<Box<Bluetooth>>>, intf: Arc<Mutex<BluetoothInterface>>, gatt: Arc<Mutex<Box<BluetoothGatt>>>, media: Arc<Mutex<Box<BluetoothMedia>>>, tx: Sender<Message>, ) -> Suspend170 pub fn new(
171 bt: Arc<Mutex<Box<Bluetooth>>>,
172 intf: Arc<Mutex<BluetoothInterface>>,
173 gatt: Arc<Mutex<Box<BluetoothGatt>>>,
174 media: Arc<Mutex<Box<BluetoothMedia>>>,
175 tx: Sender<Message>,
176 ) -> Suspend {
177 bt.lock()
178 .unwrap()
179 .register_connection_callback(Box::new(BluetoothConnectionCallbacks::new(tx.clone())));
180 Self {
181 bt,
182 intf,
183 gatt,
184 media,
185 tx: tx.clone(),
186 callbacks: Callbacks::new(tx.clone(), |id| {
187 Message::SuspendActions(SuspendActions::CallbackDisconnected(id))
188 }),
189 audio_reconnect_list: Vec::new(),
190 audio_reconnect_joinhandle: None,
191 suspend_state: Arc::new(Mutex::new(SuspendState::new())),
192 }
193 }
194
handle_action(&mut self, action: SuspendActions)195 pub(crate) fn handle_action(&mut self, action: SuspendActions) {
196 match action {
197 SuspendActions::CallbackRegistered(id) => {
198 self.callback_registered(id);
199 }
200 SuspendActions::CallbackDisconnected(id) => {
201 self.remove_callback(id);
202 }
203 SuspendActions::SuspendReady(suspend_id) => {
204 self.suspend_ready(suspend_id);
205 }
206 SuspendActions::ResumeReady(suspend_id) => {
207 self.resume_ready(suspend_id);
208 }
209 SuspendActions::AudioReconnectOnResumeComplete => {
210 self.audio_reconnect_complete();
211 }
212 SuspendActions::DeviceDisconnected(addr) => {
213 self.device_disconnected(addr);
214 }
215 }
216 }
217
callback_registered(&mut self, id: u32)218 fn callback_registered(&mut self, id: u32) {
219 match self.callbacks.get_by_id_mut(id) {
220 Some(callback) => callback.on_callback_registered(id),
221 None => warn!("Suspend callback {} does not exist", id),
222 }
223 }
224
remove_callback(&mut self, id: u32) -> bool225 fn remove_callback(&mut self, id: u32) -> bool {
226 self.callbacks.remove_callback(id)
227 }
228
suspend_ready(&mut self, suspend_id: i32)229 fn suspend_ready(&mut self, suspend_id: i32) {
230 let mut suspend_state = self.suspend_state.lock().unwrap();
231 if !suspend_state.ready_to_suspend() {
232 return;
233 }
234 if !suspend_state.suspend_expected {
235 // We might already send out a ready.
236 return;
237 }
238 suspend_state.suspend_expected = false;
239 let hci_index = self.bt.lock().unwrap().get_hci_index();
240 notify_suspend_state(hci_index, true);
241 self.callbacks.for_all_callbacks(|callback| {
242 callback.on_suspend_ready(suspend_id);
243 });
244 }
245
resume_ready(&mut self, suspend_id: i32)246 fn resume_ready(&mut self, suspend_id: i32) {
247 self.callbacks.for_all_callbacks(|callback| {
248 callback.on_resumed(suspend_id);
249 });
250 }
251
252 /// On resume, we attempt to reconnect to any audio devices connected during suspend.
253 /// This marks this attempt as completed and we should clear the pending reconnects here.
audio_reconnect_complete(&mut self)254 fn audio_reconnect_complete(&mut self) {
255 self.audio_reconnect_list.clear();
256 self.audio_reconnect_joinhandle = None;
257 }
258
device_disconnected(&mut self, addr: RawAddress)259 fn device_disconnected(&mut self, addr: RawAddress) {
260 let mut suspend_state = self.suspend_state.lock().unwrap();
261 if !suspend_state.disconnect_expected.remove(&addr) {
262 // Not interested device, or we are not suspending.
263 return;
264 }
265 if !suspend_state.disconnect_expected.is_empty() {
266 return;
267 }
268 if let Some(h) = suspend_state.disconnect_timeout_timer.take() {
269 h.abort();
270 }
271 Self::all_acls_disconnected(
272 self.tx.clone(),
273 &mut suspend_state,
274 self.suspend_state.clone(),
275 self.intf.clone(),
276 );
277 }
278
279 /// Continues the suspend process after all ACLs are disconnected.
280 ///
281 /// Two SuspendState are passed as arguments here because we want the mutex to be held during
282 /// the whole device disconnect process. Thus, the caller of this function should provide a
283 /// MutexGuard, while an Arc is also needed for this function to compose an async task.
284 ///
285 /// After ReentrantLock (https://github.com/rust-lang/rust/issues/121440) is supported, we
286 /// shall replace them with a single Arc<ReentrantLock<SuspendState>>.
all_acls_disconnected( tx: Sender<Message>, suspend_state: &mut MutexGuard<SuspendState>, suspend_state_cloned: Arc<Mutex<SuspendState>>, intf: Arc<Mutex<BluetoothInterface>>, )287 fn all_acls_disconnected(
288 tx: Sender<Message>,
289 suspend_state: &mut MutexGuard<SuspendState>,
290 suspend_state_cloned: Arc<Mutex<SuspendState>>,
291 intf: Arc<Mutex<BluetoothInterface>>,
292 ) {
293 let suspend_id = suspend_state
294 .suspend_id
295 .expect("life cycle of suspend_id must be longer than disconnect_timeout_timer");
296 let wake_allowed = suspend_state.wake_allowed;
297 suspend_state.disconnect_timeout_timer = Some(tokio::spawn(async move {
298 if wake_allowed {
299 intf.lock().unwrap().allow_wake_by_hid();
300 // Allow wake is async. Wait for a little while.
301 time::sleep(Duration::from_millis(SUSPEND_READY_DELAY_MS)).await;
302 }
303 suspend_state_cloned.lock().unwrap().disconnect_timeout_timer = None;
304 let _result =
305 tx.send(Message::SuspendActions(SuspendActions::SuspendReady(suspend_id))).await;
306 }));
307 }
308
get_connected_audio_devices(&self) -> Vec<BluetoothDevice>309 fn get_connected_audio_devices(&self) -> Vec<BluetoothDevice> {
310 let bonded_connected = self.bt.lock().unwrap().get_bonded_and_connected_devices();
311 self.media.lock().unwrap().filter_to_connected_audio_devices_from(&bonded_connected)
312 }
313 }
314
315 impl ISuspend for Suspend {
register_callback(&mut self, callback: Box<dyn ISuspendCallback + Send>) -> bool316 fn register_callback(&mut self, callback: Box<dyn ISuspendCallback + Send>) -> bool {
317 let id = self.callbacks.add_callback(callback);
318
319 let tx = self.tx.clone();
320 tokio::spawn(async move {
321 let _result =
322 tx.send(Message::SuspendActions(SuspendActions::CallbackRegistered(id))).await;
323 });
324
325 true
326 }
327
unregister_callback(&mut self, callback_id: u32) -> bool328 fn unregister_callback(&mut self, callback_id: u32) -> bool {
329 self.remove_callback(callback_id)
330 }
331
suspend(&mut self, suspend_type: SuspendType, suspend_id: i32)332 fn suspend(&mut self, suspend_type: SuspendType, suspend_id: i32) {
333 let mut suspend_state = self.suspend_state.lock().unwrap();
334 // Set suspend state as true, prevent an early resume.
335 suspend_state.suspend_expected = true;
336 suspend_state.suspend_id = Some(suspend_id);
337 // Treat Other the same as AllowWakeFromHid
338 suspend_state.wake_allowed =
339 matches!(suspend_type, SuspendType::AllowWakeFromHid | SuspendType::Other);
340
341 self.bt.lock().unwrap().scan_mode_enter_suspend();
342 self.intf.lock().unwrap().clear_event_filter();
343 self.intf.lock().unwrap().clear_filter_accept_list();
344
345 self.bt.lock().unwrap().discovery_enter_suspend();
346 self.gatt.lock().unwrap().advertising_enter_suspend();
347 self.gatt.lock().unwrap().scan_enter_suspend();
348
349 // Track connected audio devices and queue them for reconnect on resume.
350 // If we still have the previous reconnect list left-over, do not try
351 // to collect a new list here.
352 if self.audio_reconnect_list.is_empty() {
353 self.audio_reconnect_list = self.get_connected_audio_devices();
354 }
355
356 // Cancel any active reconnect task.
357 if let Some(joinhandle) = &self.audio_reconnect_joinhandle {
358 joinhandle.abort();
359 self.audio_reconnect_joinhandle = None;
360 }
361
362 // Now we have some async tasks to do and need to wait for some events.
363 // For each task we need to schedule a timeout timer to ensure we suspend eventually.
364 // When a task is done, it should remove the timer and send a SuspendReady event.
365 // |ready_to_suspend| shall check that all tasks have done (removed their timer).
366
367 // This doesn't do anything but simply wait for a short delay to ensure the above functions
368 // have finished (LibBluetooth may leave some async tasks).
369 if let Some(h) = suspend_state.delay_timer.take() {
370 log::warn!("Suspend: Found a leftover timer for delay task");
371 h.abort();
372 }
373 let tx = self.tx.clone();
374 let suspend_state_cloned = self.suspend_state.clone();
375 suspend_state.delay_timer = Some(tokio::spawn(async move {
376 time::sleep(Duration::from_millis(SUSPEND_READY_DELAY_MS)).await;
377 suspend_state_cloned.lock().unwrap().delay_timer = None;
378 let _result =
379 tx.send(Message::SuspendActions(SuspendActions::SuspendReady(suspend_id))).await;
380 }));
381
382 // Disconnect all ACLs and wait until all devices have disconnected.
383 if let Some(h) = suspend_state.disconnect_timeout_timer.take() {
384 log::warn!("Suspend: Found a leftover timer for disconnect");
385 h.abort();
386 }
387 suspend_state.disconnect_expected = HashSet::from_iter(
388 self.bt.lock().unwrap().get_connected_devices().iter().map(|d| d.address),
389 );
390 let tx = self.tx.clone();
391 let suspend_state_cloned = self.suspend_state.clone();
392 let intf_cloned = self.intf.clone();
393 if suspend_state.disconnect_expected.is_empty() {
394 // No need to set a timeout timer as no disconnection is expected.
395 Self::all_acls_disconnected(tx, &mut suspend_state, suspend_state_cloned, intf_cloned);
396 } else {
397 self.intf.lock().unwrap().disconnect_all_acls();
398 suspend_state.disconnect_timeout_timer = Some(tokio::spawn(async move {
399 time::sleep(Duration::from_millis(2000)).await;
400 log::error!("Suspend disconnect did not complete in 2s, continuing anyway.");
401 let mut suspend_state = suspend_state_cloned.lock().unwrap();
402 // Cleanup disconnect_expected so |device_disconnected| won't be triggered.
403 suspend_state.disconnect_expected = HashSet::default();
404 // Continue the suspend. There might be some disconnection events later and if the
405 // device is not in the lid-closed state, the device might be awaken. This shall be
406 // a really rare case and it's hard to handle as setting a event mask could break
407 // the state machine in LibBluetooth.
408 // We could consider increase the timeout in the future if this happens too often.
409 Self::all_acls_disconnected(
410 tx,
411 &mut suspend_state,
412 suspend_state_cloned.clone(),
413 intf_cloned,
414 );
415 }));
416 }
417 }
418
resume(&mut self) -> bool419 fn resume(&mut self) -> bool {
420 let mut suspend_state = self.suspend_state.lock().unwrap();
421 // Suspend is not ready (e.g. aborted early), delay cleanup after SuspendReady.
422 if suspend_state.suspend_expected {
423 log::error!("Suspend is expected but not ready, abort resume.");
424 return false;
425 }
426
427 // Suspend ID state 0: NoRecord, 1: Recorded
428 let suspend_id = match suspend_state.suspend_id {
429 None => {
430 log::error!("No suspend id saved at resume.");
431 metrics::suspend_complete_state(0);
432 // If no suspend id is saved here, it means floss did not receive the
433 // SuspendImminent signal and as a result, the suspend flow was not run.
434 // Skip the resume flow and return after logging the metrics.
435 return true;
436 }
437 Some(id) => {
438 metrics::suspend_complete_state(1);
439 id
440 }
441 };
442
443 let hci_index = self.bt.lock().unwrap().get_hci_index();
444 notify_suspend_state(hci_index, false);
445
446 self.intf.lock().unwrap().set_default_event_mask_except(0u64, 0u64);
447
448 // Restore event filter and accept list to normal.
449 self.intf.lock().unwrap().clear_event_filter();
450 self.intf.lock().unwrap().clear_filter_accept_list();
451 self.intf.lock().unwrap().restore_filter_accept_list();
452 self.bt.lock().unwrap().scan_mode_exit_suspend();
453
454 if !self.audio_reconnect_list.is_empty() {
455 let reconnect_list = self.audio_reconnect_list.clone();
456 let txl = self.tx.clone();
457
458 // Cancel any existing reconnect attempt.
459 if let Some(joinhandle) = &self.audio_reconnect_joinhandle {
460 joinhandle.abort();
461 self.audio_reconnect_joinhandle = None;
462 }
463
464 self.audio_reconnect_joinhandle = Some(tokio::spawn(async move {
465 // Wait a few seconds to avoid co-ex issues with wi-fi.
466 time::sleep(Duration::from_millis(RECONNECT_AUDIO_ON_RESUME_DELAY_MS)).await;
467
468 // Queue up connections.
469 for device in reconnect_list {
470 let _unused: Option<()> = txl
471 .send(Message::AdapterActions(AdapterActions::ConnectAllProfiles(device)))
472 .await
473 .ok();
474 }
475
476 // Mark that we're done.
477 let _unused: Option<()> = txl
478 .send(Message::SuspendActions(SuspendActions::AudioReconnectOnResumeComplete))
479 .await
480 .ok();
481 }));
482 }
483
484 self.bt.lock().unwrap().discovery_exit_suspend();
485 self.gatt.lock().unwrap().advertising_exit_suspend();
486 self.gatt.lock().unwrap().scan_exit_suspend();
487
488 let tx = self.tx.clone();
489 tokio::spawn(async move {
490 let _result =
491 tx.send(Message::SuspendActions(SuspendActions::ResumeReady(suspend_id))).await;
492 });
493
494 true
495 }
496 }
497
498 struct BluetoothConnectionCallbacks {
499 tx: Sender<Message>,
500 }
501
502 impl BluetoothConnectionCallbacks {
new(tx: Sender<Message>) -> Self503 fn new(tx: Sender<Message>) -> Self {
504 Self { tx }
505 }
506 }
507
508 impl IBluetoothConnectionCallback for BluetoothConnectionCallbacks {
on_device_connected(&mut self, _device: BluetoothDevice)509 fn on_device_connected(&mut self, _device: BluetoothDevice) {}
510
on_device_disconnected(&mut self, device: BluetoothDevice)511 fn on_device_disconnected(&mut self, device: BluetoothDevice) {
512 let tx = self.tx.clone();
513 tokio::spawn(async move {
514 let _result = tx
515 .send(Message::SuspendActions(SuspendActions::DeviceDisconnected(device.address)))
516 .await;
517 });
518 }
519
on_device_connection_failed(&mut self, _device: BluetoothDevice, _status: BtStatus)520 fn on_device_connection_failed(&mut self, _device: BluetoothDevice, _status: BtStatus) {}
521 }
522
523 impl RPCProxy for BluetoothConnectionCallbacks {
get_object_id(&self) -> String524 fn get_object_id(&self) -> String {
525 "Bluetooth Connection Callback".to_string()
526 }
527 }
528