settings/agent/earcons/
bluetooth_handler.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::agent::earcons::agent::CommonEarconsParams;
6use crate::agent::earcons::sound_ids::{
7    BLUETOOTH_CONNECTED_SOUND_ID, BLUETOOTH_DISCONNECTED_SOUND_ID,
8};
9use crate::agent::earcons::utils::{connect_to_sound_player, play_sound};
10use crate::audio::Request as AudioRequest;
11use crate::audio::types::{AudioSettingSource, AudioStreamType, SetAudioStream};
12use anyhow::{Context, Error, format_err};
13use fidl::endpoints::create_request_stream;
14use fidl_fuchsia_media_sessions2::{
15    DiscoveryMarker, SessionsWatcherRequest, SessionsWatcherRequestStream, WatchOptions,
16};
17use futures::channel::mpsc::UnboundedSender;
18use futures::channel::oneshot;
19use futures::stream::TryStreamExt;
20use settings_common::inspect::event::ExternalEventPublisher;
21use settings_common::{call, trace};
22use std::collections::HashSet;
23use {fuchsia_async as fasync, fuchsia_trace as ftrace};
24
25/// Type for uniquely identifying bluetooth media sessions.
26type SessionId = u64;
27
28/// The file path for the earcon to be played for bluetooth connecting.
29const BLUETOOTH_CONNECTED_FILE_PATH: &str = "bluetooth-connected.wav";
30
31/// The file path for the earcon to be played for bluetooth disconnecting.
32const BLUETOOTH_DISCONNECTED_FILE_PATH: &str = "bluetooth-disconnected.wav";
33
34pub(crate) const BLUETOOTH_DOMAIN: &str = "Bluetooth";
35
36/// The `BluetoothHandler` takes care of the earcons functionality on bluetooth connection
37/// and disconnection.
38pub(super) struct BluetoothHandler {
39    // Parameters common to all earcons handlers.
40    common_earcons_params: CommonEarconsParams,
41    audio_request_tx: Option<UnboundedSender<AudioRequest>>,
42    // The publisher to use for connecting to services.
43    external_publisher: ExternalEventPublisher,
44    // The ids of the media sessions that are currently active.
45    active_sessions: HashSet<SessionId>,
46}
47
48impl std::fmt::Debug for BluetoothHandler {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("BluetoothHandler")
51            .field("common_earcons_params", &self.common_earcons_params)
52            .field("audio_request_tx", &self.audio_request_tx)
53            .field("active_sessions", &self.active_sessions)
54            .finish_non_exhaustive()
55    }
56}
57
58/// The type of bluetooth earcons sound.
59enum BluetoothSoundType {
60    Connected,
61    Disconnected,
62}
63
64impl BluetoothHandler {
65    pub(super) async fn spawn(
66        audio_request_tx: Option<UnboundedSender<AudioRequest>>,
67        external_publisher: ExternalEventPublisher,
68        params: CommonEarconsParams,
69    ) -> Result<(), Error> {
70        let mut handler = Self {
71            audio_request_tx,
72            common_earcons_params: params,
73            external_publisher,
74            active_sessions: HashSet::<SessionId>::new(),
75        };
76        handler.watch_bluetooth_connections().await
77    }
78
79    /// Watch for media session changes. The media sessions that have the
80    /// Bluetooth mode in their metadata signify a bluetooth connection.
81    /// The id of a disconnected device will be received on removal.
82    pub(super) async fn watch_bluetooth_connections(&mut self) -> Result<(), Error> {
83        // Connect to media session Discovery service.
84        let discovery_connection_result = self
85            .common_earcons_params
86            .service_context
87            .connect_with_publisher::<DiscoveryMarker, _>(self.external_publisher.clone())
88            .await
89            .context("Connecting to fuchsia.media.sessions2.Discovery");
90
91        let discovery_proxy = discovery_connection_result.map_err(|e| {
92            format_err!("Failed to connect to fuchsia.media.sessions2.Discovery: {:?}", e)
93        })?;
94
95        // Create and handle the request stream of media sessions.
96        let (watcher_client, watcher_requests) = create_request_stream();
97
98        call!(discovery_proxy =>
99            watch_sessions(&WatchOptions::default(), watcher_client))
100        .map_err(|e| format_err!("Unable to start discovery of MediaSessions: {:?}", e))?;
101
102        self.handle_bluetooth_connections(watcher_requests);
103        Ok(())
104    }
105
106    /// Handles the stream of media session updates, and possibly plays earcons
107    /// sounds based on what type of update is received.
108    fn handle_bluetooth_connections(&mut self, mut watcher_requests: SessionsWatcherRequestStream) {
109        let audio_request_tx = self.audio_request_tx.clone();
110        let mut active_sessions_clone = self.active_sessions.clone();
111        let external_publisher = self.external_publisher.clone();
112        let common_earcons_params = self.common_earcons_params.clone();
113
114        fasync::Task::local(async move {
115            loop {
116                let maybe_req = watcher_requests.try_next().await;
117                match maybe_req {
118                    Ok(Some(req)) => {
119                        match req {
120                            SessionsWatcherRequest::SessionUpdated {
121                                session_id: id,
122                                session_info_delta: delta,
123                                responder,
124                            } => {
125                                if let Err(e) = responder.send() {
126                                    log::error!("Failed to acknowledge delta from SessionWatcher: {:?}", e);
127                                    return;
128                                }
129
130                                if active_sessions_clone.contains(&id)
131                                    || !matches!(delta.domain, Some(name) if name == BLUETOOTH_DOMAIN)
132                                {
133                                    continue;
134                                }
135                                let _ = active_sessions_clone.insert(id);
136
137                                let audio_request_tx = audio_request_tx.clone();
138                                let external_publisher = external_publisher.clone();
139                                let common_earcons_params = common_earcons_params.clone();
140                                fasync::Task::local(async move {
141                                    play_bluetooth_sound(
142                                        common_earcons_params,
143                                        audio_request_tx,
144                                        external_publisher,
145                                        BluetoothSoundType::Connected,
146                                    )
147                                    .await;
148                                })
149                                .detach();
150                            }
151                            SessionsWatcherRequest::SessionRemoved { session_id, responder } => {
152                                if let Err(e) = responder.send() {
153                                    log::error!(
154                                        "Failed to acknowledge session removal from SessionWatcher: {:?}",
155                                        e
156                                    );
157                                    return;
158                                }
159
160                                if !active_sessions_clone.contains(&session_id) {
161                                    log::warn!(
162                                        "Tried to remove nonexistent media session id {:?}",
163                                        session_id
164                                    );
165                                    continue;
166                                }
167                                let _ = active_sessions_clone.remove(&session_id);
168                                let audio_request_tx = audio_request_tx.clone();
169                                let external_publisher = external_publisher.clone();
170                                let common_earcons_params = common_earcons_params.clone();
171                                fasync::Task::local(async move {
172                                    play_bluetooth_sound(
173                                        common_earcons_params,
174                                        audio_request_tx,
175                                        external_publisher,
176                                        BluetoothSoundType::Disconnected,
177                                    )
178                                    .await;
179                                })
180                                .detach();
181                            }
182                        }
183                    },
184                    Ok(None) => {
185                        log::warn!("stream ended on fuchsia.media.sessions2.SessionsWatcher");
186                        break;
187                    },
188                    Err(e) => {
189                        log::error!("failed to watch fuchsia.media.sessions2.SessionsWatcher: {:?}", &e);
190                        break;
191                    },
192                }
193            }
194        })
195        .detach();
196    }
197}
198
199/// Play a bluetooth earcons sound.
200async fn play_bluetooth_sound(
201    common_earcons_params: CommonEarconsParams,
202    audio_request_tx: Option<UnboundedSender<AudioRequest>>,
203    external_publisher: ExternalEventPublisher,
204    sound_type: BluetoothSoundType,
205) {
206    // Connect to the SoundPlayer if not already connected.
207    connect_to_sound_player(
208        external_publisher,
209        common_earcons_params.service_context.clone(),
210        common_earcons_params.sound_player_connection.clone(),
211    )
212    .await;
213
214    let sound_player_connection = common_earcons_params.sound_player_connection.clone();
215    let sound_player_connection_lock = sound_player_connection.lock().await;
216    let sound_player_added_files = common_earcons_params.sound_player_added_files.clone();
217
218    if let Some(sound_player_proxy) = sound_player_connection_lock.as_ref() {
219        match_background_to_media(audio_request_tx).await;
220        match sound_type {
221            BluetoothSoundType::Connected => {
222                if play_sound(
223                    sound_player_proxy,
224                    BLUETOOTH_CONNECTED_FILE_PATH,
225                    BLUETOOTH_CONNECTED_SOUND_ID,
226                    sound_player_added_files.clone(),
227                )
228                .await
229                .is_err()
230                {
231                    log::error!(
232                        "[bluetooth_earcons_handler] failed to play bluetooth earcon connection sound"
233                    );
234                }
235            }
236            BluetoothSoundType::Disconnected => {
237                if play_sound(
238                    sound_player_proxy,
239                    BLUETOOTH_DISCONNECTED_FILE_PATH,
240                    BLUETOOTH_DISCONNECTED_SOUND_ID,
241                    sound_player_added_files.clone(),
242                )
243                .await
244                .is_err()
245                {
246                    log::error!(
247                        "[bluetooth_earcons_handler] failed to play bluetooth earcon disconnection sound"
248                    );
249                }
250            }
251        };
252    } else {
253        log::error!(
254            "[bluetooth_earcons_handler] failed to play bluetooth earcon sound: no sound player connection"
255        );
256    }
257}
258
259/// Match the background volume to the current media volume before playing the bluetooth earcon.
260async fn match_background_to_media(audio_request_tx: Option<UnboundedSender<AudioRequest>>) {
261    let info = if let Some(audio_request_tx) = audio_request_tx.as_ref() {
262        let (tx, rx) = oneshot::channel();
263        if audio_request_tx.unbounded_send(AudioRequest::Get(ftrace::Id::new(), tx)).is_ok() {
264            rx.await.ok()
265        } else {
266            None
267        }
268    } else {
269        None
270    };
271    // Extract media and background volumes.
272    let mut media_volume = 0.0;
273    let mut background_volume = 0.0;
274    if let Some(info) = info {
275        for stream in &info.streams {
276            if stream.stream_type == AudioStreamType::Media {
277                media_volume = stream.user_volume_level;
278            } else if stream.stream_type == AudioStreamType::Background {
279                background_volume = stream.user_volume_level;
280            }
281        }
282    } else {
283        log::error!("Could not extract background and media volumes")
284    }
285
286    // If they are different, set the background volume to match the media volume.
287    if media_volume != background_volume {
288        let id = ftrace::Id::new();
289        trace!(id, c"bluetooth_handler set background volume");
290        let streams = vec![SetAudioStream {
291            stream_type: AudioStreamType::Background,
292            source: AudioSettingSource::System,
293            user_volume_level: Some(media_volume),
294            user_volume_muted: None,
295        }];
296        if let Some(audio_request_tx) = audio_request_tx {
297            let (tx, rx) = oneshot::channel();
298            if audio_request_tx.unbounded_send(AudioRequest::Set(streams, id, tx)).is_ok() {
299                if let Err(e) = rx.await {
300                    log::error!(
301                        "Failed to play bluetooth connection sound after waiting for request response: {e:?}"
302                    );
303                }
304            }
305        }
306    }
307}