settings/agent/earcons/
volume_change_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::{VOLUME_CHANGED_SOUND_ID, VOLUME_MAX_SOUND_ID};
7use crate::agent::earcons::utils::{connect_to_sound_player, play_sound};
8use crate::audio::types::{
9    AUDIO_STREAM_TYPE_COUNT, AudioInfo, AudioSettingSource, AudioStream, AudioStreamType,
10    SetAudioStream,
11};
12use crate::audio::{ModifiedCounters, Request as AudioRequest, create_default_modified_counters};
13use anyhow::Error;
14use futures::StreamExt;
15use futures::channel::mpsc::{self, UnboundedSender};
16use futures::channel::oneshot;
17use futures::future::OptionFuture;
18use settings_common::inspect::event::ExternalEventPublisher;
19use settings_common::trace;
20use std::collections::{HashMap, HashSet};
21use std::rc::Rc;
22use {fuchsia_async as fasync, fuchsia_trace as ftrace};
23
24/// The `VolumeChangeHandler` takes care of the earcons functionality on volume change.
25pub(super) struct VolumeChangeHandler {
26    common_earcons_params: CommonEarconsParams,
27    last_user_volumes: HashMap<AudioStreamType, f32>,
28    modified_counters: ModifiedCounters,
29    external_publisher: ExternalEventPublisher,
30    audio_request_tx: Option<UnboundedSender<AudioRequest>>,
31}
32
33/// The maximum volume level.
34const MAX_VOLUME: f32 = 1.0;
35
36/// The file path for the earcon to be played for max sound level.
37const VOLUME_MAX_FILE_PATH: &str = "volume-max.wav";
38
39/// The file path for the earcon to be played for volume changes below max volume level.
40const VOLUME_CHANGED_FILE_PATH: &str = "volume-changed.wav";
41
42impl VolumeChangeHandler {
43    pub(super) async fn spawn(
44        audio_request_tx: Option<UnboundedSender<AudioRequest>>,
45        external_publisher: ExternalEventPublisher,
46        params: CommonEarconsParams,
47    ) -> Result<(), Error> {
48        let info = if let Some(request_tx) = audio_request_tx.as_ref() {
49            let (tx, rx) = oneshot::channel();
50            if request_tx.unbounded_send(AudioRequest::Get(ftrace::Id::new(), tx)).is_ok() {
51                rx.await.ok()
52            } else {
53                None
54            }
55        } else {
56            None
57        };
58        let last_user_volumes = info
59            .map(|info| {
60                // Create map from stream type to user volume levels for each stream.
61                info.streams
62                    .iter()
63                    .filter(|x| {
64                        x.stream_type == AudioStreamType::Media
65                            || x.stream_type == AudioStreamType::Interruption
66                    })
67                    .map(|stream| (stream.stream_type, stream.user_volume_level))
68                    .collect()
69            })
70            .unwrap_or_else(|| HashMap::new());
71
72        fasync::Task::local(async move {
73            let mut handler = Self {
74                common_earcons_params: params,
75                last_user_volumes,
76                modified_counters: create_default_modified_counters(),
77                external_publisher,
78                audio_request_tx: audio_request_tx.clone(),
79            };
80
81            let mut audio_rx = audio_request_tx.as_ref().and_then(|request_tx| {
82                let (audio_tx, audio_rx) = mpsc::unbounded();
83                request_tx.unbounded_send(AudioRequest::Listen(audio_tx)).ok().map(|_| audio_rx)
84            });
85
86            let mut audio_next = OptionFuture::from(audio_rx.as_mut().map(|rx| rx.next()));
87
88            loop {
89                futures::select! {
90                    audio_info = audio_next => {
91                        if let Some(Some(audio_info)) = audio_info {
92                            handler.on_audio_info(audio_info).await;
93                            audio_next = OptionFuture::from(audio_rx.as_mut().map(|rx| rx.next()));
94                        }
95                    }
96                    complete => break,
97                }
98            }
99        })
100        .detach();
101
102        Ok(())
103    }
104
105    /// Calculates and returns the streams that were changed based on
106    /// their timestamps, updating them in the stored timestamps if
107    /// they were changed.
108    fn calculate_changed_streams(
109        &mut self,
110        all_streams: [AudioStream; AUDIO_STREAM_TYPE_COUNT],
111        new_modified_counters: ModifiedCounters,
112    ) -> Vec<AudioStream> {
113        let mut changed_stream_types = HashSet::new();
114        for (stream_type, timestamp) in new_modified_counters {
115            if self.modified_counters.get(&stream_type) != Some(&timestamp) {
116                let _ = changed_stream_types.insert(stream_type);
117                let _ = self.modified_counters.insert(stream_type, timestamp);
118            }
119        }
120
121        IntoIterator::into_iter(all_streams)
122            .filter(|stream| changed_stream_types.contains(&stream.stream_type))
123            .collect()
124    }
125
126    /// Retrieve a user volume of the specified `stream_type` from the given `changed_streams`.
127    fn get_user_volume(
128        &self,
129        changed_streams: Vec<AudioStream>,
130        stream_type: AudioStreamType,
131    ) -> Option<f32> {
132        changed_streams.iter().find(|&&x| x.stream_type == stream_type).map(|x| x.user_volume_level)
133    }
134
135    /// Retrieve the change source of the specified `stream_type` from the given `changed_streams`.
136    fn get_change_source(
137        &self,
138        changed_streams: Vec<AudioStream>,
139        stream_type: AudioStreamType,
140    ) -> Option<AudioSettingSource> {
141        changed_streams.iter().find(|&&x| x.stream_type == stream_type).map(|x| x.source)
142    }
143
144    /// Helper for on_audio_info. Handles the changes for a specific AudioStreamType.
145    /// Enables separate handling of earcons on different streams.
146    async fn on_audio_info_for_stream(
147        &mut self,
148        new_user_volume: f32,
149        stream_type: AudioStreamType,
150        change_source: Option<AudioSettingSource>,
151    ) {
152        let volume_is_max = new_user_volume == MAX_VOLUME;
153        let last_user_volume = self.last_user_volumes.get(&stream_type);
154
155        // Logging for debugging volume changes.
156        log::debug!(
157            "[earcons_agent] New {:?} user volume: {:?}, Last {:?} user volume: {:?}",
158            stream_type,
159            new_user_volume,
160            stream_type,
161            last_user_volume,
162        );
163
164        if last_user_volume != Some(&new_user_volume) || volume_is_max {
165            // On restore, the last media user volume is set for the first time, and registers
166            // as different from the last seen volume, because it is initially None. Don't play
167            // the earcons sound on that set.
168            //
169            // If the change_source is System, do not play a sound since the user doesn't need
170            // to hear feedback for such changes. For system sounds that need to play earcons,
171            // the source should be AudioSettingSource::SystemWithFeedback. An example
172            // of a system change is when the volume is reset after night mode deactivates.
173            if last_user_volume.is_some() && change_source != Some(AudioSettingSource::System) {
174                let id = ftrace::Id::new();
175                trace!(id, "volume_change_handler set background");
176                let streams = vec![SetAudioStream {
177                    stream_type: AudioStreamType::Background,
178                    source: AudioSettingSource::System,
179                    user_volume_level: Some(new_user_volume),
180                    user_volume_muted: None,
181                }];
182                let success = if let Some(audio_request_tx) = self.audio_request_tx.as_ref() {
183                    let (tx, rx) = oneshot::channel();
184                    if audio_request_tx.unbounded_send(AudioRequest::Set(streams, id, tx)).is_ok() {
185                        rx.await
186                            .map_err(|e| {
187                                log::error!(
188                                    "Failed to play sound after waiting for volume set: {e:?}"
189                                )
190                            })
191                            .is_ok()
192                    } else {
193                        false
194                    }
195                } else {
196                    false
197                };
198                if success {
199                    self.play_volume_sound(new_user_volume);
200                }
201            }
202
203            let _ = self.last_user_volumes.insert(stream_type, new_user_volume);
204        }
205    }
206
207    /// Invoked when a new `AudioInfo` is retrieved. Determines whether an
208    /// earcon should be played and plays sound if necessary.
209    async fn on_audio_info(&mut self, audio_info: AudioInfo) {
210        let changed_streams = match audio_info.modified_counters {
211            None => Vec::new(),
212            Some(counters) => self.calculate_changed_streams(audio_info.streams, counters),
213        };
214
215        let media_user_volume =
216            self.get_user_volume(changed_streams.clone(), AudioStreamType::Media);
217        let interruption_user_volume =
218            self.get_user_volume(changed_streams.clone(), AudioStreamType::Interruption);
219        let media_change_source =
220            self.get_change_source(changed_streams.clone(), AudioStreamType::Media);
221
222        if let Some(media_user_volume) = media_user_volume {
223            self.on_audio_info_for_stream(
224                media_user_volume,
225                AudioStreamType::Media,
226                media_change_source,
227            )
228            .await;
229        }
230        if let Some(interruption_user_volume) = interruption_user_volume {
231            self.on_audio_info_for_stream(
232                interruption_user_volume,
233                AudioStreamType::Interruption,
234                None,
235            )
236            .await;
237        }
238    }
239
240    /// Play the earcons sound given the changed volume streams.
241    fn play_volume_sound(&self, volume: f32) {
242        let external_publisher = self.external_publisher.clone();
243        let common_earcons_params = self.common_earcons_params.clone();
244
245        fasync::Task::local(async move {
246            // Connect to the SoundPlayer if not already connected.
247            connect_to_sound_player(
248                external_publisher,
249                common_earcons_params.service_context,
250                Rc::clone(&common_earcons_params.sound_player_connection),
251            )
252            .await;
253
254            let sound_player_connection = common_earcons_params.sound_player_connection;
255            let sound_player_connection = sound_player_connection.lock().await;
256            let sound_player_added_files = common_earcons_params.sound_player_added_files;
257
258            if let (Some(sound_player_proxy), volume_level) =
259                (sound_player_connection.as_ref(), volume)
260            {
261                let play_sound_result = if volume_level >= 1.0 {
262                    play_sound(
263                        sound_player_proxy,
264                        VOLUME_MAX_FILE_PATH,
265                        VOLUME_MAX_SOUND_ID,
266                        sound_player_added_files.clone(),
267                    )
268                    .await
269                } else if volume_level > 0.0 {
270                    play_sound(
271                        sound_player_proxy,
272                        VOLUME_CHANGED_FILE_PATH,
273                        VOLUME_CHANGED_SOUND_ID,
274                        sound_player_added_files.clone(),
275                    )
276                    .await
277                } else {
278                    Ok(())
279                };
280                if let Err(e) = play_sound_result {
281                    log::warn!("Failed to play sound: {:?}", e);
282                }
283            }
284        })
285        .detach();
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::audio::build_audio_default_settings;
293    use fuchsia_inspect::component;
294    use futures::lock::Mutex;
295    use settings_common::inspect::config_logger::InspectConfigLogger;
296    use settings_common::service_context::ServiceContext;
297    use std::rc::Rc;
298
299    fn fake_values() -> (
300        [AudioStream; AUDIO_STREAM_TYPE_COUNT], // fake_streams
301        ModifiedCounters,                       // old_counters
302        ModifiedCounters,                       // new_counters
303        Vec<AudioStream>,                       // expected_changed_streams
304    ) {
305        let config_logger =
306            Rc::new(std::sync::Mutex::new(InspectConfigLogger::new(component::inspector().root())));
307        let mut settings = build_audio_default_settings(config_logger);
308        let settings = settings
309            .load_default_value()
310            .expect("config data should exist and be parseable for tests")
311            .unwrap();
312        let fake_streams = settings.streams;
313        let old_timestamps = create_default_modified_counters();
314        let new_timestamps = [
315            (AudioStreamType::Background, 0),
316            (AudioStreamType::Media, 1),
317            (AudioStreamType::Interruption, 0),
318            (AudioStreamType::SystemAgent, 2),
319            (AudioStreamType::Communication, 3),
320            (AudioStreamType::Accessibility, 4),
321        ]
322        .iter()
323        .cloned()
324        .collect();
325        let expected_changed_streams =
326            [fake_streams[1], fake_streams[3], fake_streams[4], fake_streams[5]].to_vec();
327        (fake_streams, old_timestamps, new_timestamps, expected_changed_streams)
328    }
329
330    #[fuchsia::test(allow_stalls = false)]
331    async fn test_changed_streams() {
332        let (fake_streams, old_timestamps, new_timestamps, expected_changed_streams) =
333            fake_values();
334        let (event_tx, _) = mpsc::unbounded();
335        let external_publisher = ExternalEventPublisher::new(event_tx);
336        let last_user_volumes: HashMap<_, _> =
337            [(AudioStreamType::Media, 1.0), (AudioStreamType::Interruption, 0.5)].into();
338
339        let mut handler = VolumeChangeHandler {
340            common_earcons_params: CommonEarconsParams {
341                service_context: Rc::new(ServiceContext::new(None)),
342                sound_player_added_files: Rc::new(Mutex::new(HashSet::new())),
343                sound_player_connection: Rc::new(Mutex::new(None)),
344            },
345            last_user_volumes,
346            modified_counters: old_timestamps,
347            external_publisher,
348            audio_request_tx: None,
349        };
350        let changed_streams = handler.calculate_changed_streams(fake_streams, new_timestamps);
351        assert_eq!(changed_streams, expected_changed_streams);
352    }
353}