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