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 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
25type SessionId = u64;
27
28const BLUETOOTH_CONNECTED_FILE_PATH: &str = "bluetooth-connected.wav";
30
31const BLUETOOTH_DISCONNECTED_FILE_PATH: &str = "bluetooth-disconnected.wav";
33
34pub(crate) const BLUETOOTH_DOMAIN: &str = "Bluetooth";
35
36pub(super) struct BluetoothHandler {
39 common_earcons_params: CommonEarconsParams,
41 audio_request_tx: Option<UnboundedSender<AudioRequest>>,
42 external_publisher: ExternalEventPublisher,
44 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
58enum 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 pub(super) async fn watch_bluetooth_connections(&mut self) -> Result<(), Error> {
83 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 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 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
199async 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_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
259async 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 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 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}