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::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
27type SessionId = u64;
29
30const BLUETOOTH_CONNECTED_FILE_PATH: &str = "bluetooth-connected.wav";
32
33const BLUETOOTH_DISCONNECTED_FILE_PATH: &str = "bluetooth-disconnected.wav";
35
36pub(crate) const BLUETOOTH_DOMAIN: &str = "Bluetooth";
37
38#[derive(Debug)]
41pub(super) struct BluetoothHandler {
42 common_earcons_params: CommonEarconsParams,
44 publisher: Publisher,
46 active_sessions: HashSet<SessionId>,
48 messenger: service::message::Messenger,
50}
51
52enum 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 pub(super) async fn watch_bluetooth_connections(&mut self) -> Result<(), Error> {
77 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 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 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
193async 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_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
247async fn match_background_to_media(messenger: service::message::Messenger) {
249 let mut get_receptor = messenger.message(
251 Payload::Request(Request::Get).into(),
252 Audience::Address(service::Address::Handler(SettingType::Audio)),
253 );
254
255 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 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}