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