settings/agent/earcons/
bluetooth_handler.rs1use 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
26type SessionId = u64;
28
29const BLUETOOTH_CONNECTED_FILE_PATH: &str = "bluetooth-connected.wav";
31
32const BLUETOOTH_DISCONNECTED_FILE_PATH: &str = "bluetooth-disconnected.wav";
34
35pub(crate) const BLUETOOTH_DOMAIN: &str = "Bluetooth";
36
37pub(super) struct BluetoothHandler {
40 common_earcons_params: CommonEarconsParams,
42 audio_request_tx: Option<UnboundedSender<AudioRequest>>,
43 external_publisher: ExternalEventPublisher,
45 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
59enum 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 pub(super) async fn watch_bluetooth_connections(&mut self) -> Result<(), Error> {
84 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 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 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
200async 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_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
260async 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 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 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}