Skip to main content

settings_intl/
intl_controller.rs

1// Copyright 2019 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::intl_fidl_handler::Publisher;
6use crate::types::{HourCycle, IntlInfo, LocaleId, TemperatureUnit};
7use anyhow::Error;
8use futures::StreamExt;
9use futures::channel::mpsc::UnboundedReceiver;
10use futures::channel::oneshot::Sender;
11use settings_common::inspect::event::{ResponseType, SettingValuePublisher};
12use settings_common::utils::Merge;
13use settings_storage::UpdateState;
14use settings_storage::device_storage::{DeviceStorage, DeviceStorageCompatible};
15use settings_storage::fidl_storage::FidlStorageConvertible;
16use settings_storage::storage_factory::{NoneT, StorageAccess, StorageFactory};
17use std::collections::HashSet;
18use std::rc::Rc;
19use {fuchsia_async as fasync, rust_icu_uenum as uenum, rust_icu_uloc as uloc};
20
21impl DeviceStorageCompatible for IntlInfo {
22    type Loader = NoneT;
23    const KEY: &'static str = "intl_info";
24}
25
26impl FidlStorageConvertible for IntlInfo {
27    type Storable = fidl_fuchsia_settings::IntlSettings;
28    type Loader = NoneT;
29    const KEY: &'static str = "intl";
30
31    fn to_storable(self) -> Self::Storable {
32        self.into()
33    }
34
35    fn from_storable(storable: Self::Storable) -> Self {
36        storable.into()
37    }
38}
39
40impl Default for IntlInfo {
41    fn default() -> Self {
42        IntlInfo {
43            // `-x-fxdef` is a private use extension and a special marker denoting that the
44            // setting is a fallback default, and not actually set through any user action.
45            locales: Some(vec![LocaleId { id: "en-US-x-fxdef".to_string() }]),
46            temperature_unit: Some(TemperatureUnit::Celsius),
47            time_zone_id: Some("UTC".to_string()),
48            hour_cycle: Some(HourCycle::H12),
49        }
50    }
51}
52
53#[derive(thiserror::Error, Debug)]
54pub(crate) enum IntlError {
55    #[error("Invalid argument for intl: argument:{0:?} value:{1:?}")]
56    InvalidArgument(&'static str, String),
57    #[error("Write failed for Intl: {0:?}")]
58    WriteFailure(Error),
59}
60
61impl From<&IntlError> for ResponseType {
62    fn from(error: &IntlError) -> Self {
63        match error {
64            IntlError::InvalidArgument(..) => ResponseType::InvalidArgument,
65            IntlError::WriteFailure(..) => ResponseType::StorageFailure,
66        }
67    }
68}
69
70pub(crate) enum Request {
71    Set(IntlInfo, Sender<Result<(), IntlError>>),
72}
73
74pub struct IntlController {
75    store: Rc<DeviceStorage>,
76    time_zone_ids: std::collections::HashSet<String>,
77    publisher: Option<Publisher>,
78    setting_value_publisher: SettingValuePublisher<IntlInfo>,
79}
80
81impl StorageAccess for IntlController {
82    type Storage = DeviceStorage;
83    type Data = IntlInfo;
84    const STORAGE_KEY: &'static str = <IntlInfo as DeviceStorageCompatible>::KEY;
85}
86
87/// Controller for processing requests surrounding the Intl protocol, backed by a number of
88/// services, including TimeZone.
89impl IntlController {
90    pub(super) async fn new<F>(
91        storage_factory: Rc<F>,
92        setting_value_publisher: SettingValuePublisher<IntlInfo>,
93    ) -> Self
94    where
95        F: StorageFactory<Storage = DeviceStorage>,
96    {
97        IntlController {
98            store: storage_factory.get_store().await,
99            time_zone_ids: Self::load_time_zones(),
100            publisher: None,
101            setting_value_publisher,
102        }
103    }
104
105    pub(super) fn register_publisher(&mut self, publisher: Publisher) {
106        self.publisher = Some(publisher);
107    }
108
109    fn publish(&self, info: IntlInfo) {
110        let _ = self.setting_value_publisher.publish(&info);
111        if let Some(publisher) = self.publisher.as_ref() {
112            publisher.set(info);
113        }
114    }
115
116    pub(super) async fn handle(
117        self,
118        mut request_rx: UnboundedReceiver<Request>,
119    ) -> fasync::Task<()> {
120        fasync::Task::local(async move {
121            while let Some(request) = request_rx.next().await {
122                let Request::Set(info, tx) = request;
123                let res = self.set(info).await.map(|info| {
124                    if let Some(info) = info {
125                        self.publish(info);
126                    }
127                });
128                let _ = tx.send(res);
129            }
130        })
131    }
132
133    /// Loads the set of valid time zones from resources.
134    fn load_time_zones() -> std::collections::HashSet<String> {
135        let _icu_data_loader = icu_data::Loader::new().expect("icu data loaded");
136
137        let time_zone_list = match uenum::open_time_zones() {
138            Ok(time_zones) => time_zones,
139            Err(err) => {
140                log::error!("Unable to load time zones: {:?}", err);
141                return HashSet::new();
142            }
143        };
144
145        time_zone_list.flatten().collect()
146    }
147
148    async fn set(&self, info: IntlInfo) -> Result<Option<IntlInfo>, IntlError> {
149        self.validate_intl_info(&info)?;
150
151        let current = self.store.get::<IntlInfo>().await;
152        let merged = current.merge(info);
153        self.store
154            .write(&merged)
155            .await
156            .map(|state| (UpdateState::Updated == state).then_some(merged))
157            .map_err(IntlError::WriteFailure)
158    }
159
160    /// Checks if the given IntlInfo is valid.
161    fn validate_intl_info(&self, info: &IntlInfo) -> Result<(), IntlError> {
162        if let Some(time_zone_id) = &info.time_zone_id {
163            // Make sure the given time zone ID is valid.
164            if !self.time_zone_ids.contains(time_zone_id.as_str()) {
165                return Err(IntlError::InvalidArgument("timezone id", time_zone_id.clone()));
166            }
167        }
168
169        if let Some(time_zone_locale) = &info.locales {
170            for locale in time_zone_locale {
171                // NB: `try_from` doesn't actually do validation, `for_language_tag` does but doesn't
172                // actually generate an error, it just ends up falling back to an empty string.
173                let loc = uloc::ULoc::for_language_tag(locale.id.as_str());
174                match loc {
175                    Ok(parsed) => {
176                        if parsed.label().is_empty() {
177                            log::error!("Locale is invalid: {:?}", locale.id);
178                            return Err(IntlError::InvalidArgument("locale id", locale.id.clone()));
179                        }
180                    }
181                    Err(err) => {
182                        log::error!("Error loading locale: {:?}", err);
183                        return Err(IntlError::InvalidArgument("locale id", locale.id.clone()));
184                    }
185                }
186            }
187        }
188
189        Ok(())
190    }
191
192    pub(crate) async fn restore(&self) -> IntlInfo {
193        self.store.get::<IntlInfo>().await
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use futures::channel::{mpsc, oneshot};
201    use settings_test_common::storage::InMemoryStorageFactory;
202
203    #[fuchsia::test]
204    async fn set_one() {
205        let storage_factory = InMemoryStorageFactory::new();
206        storage_factory.initialize::<IntlController>().await.expect("should initialize storage");
207        let storage_factory = Rc::new(storage_factory);
208
209        let (tx, _rx) = mpsc::unbounded();
210        let setting_value_publisher = SettingValuePublisher::new(tx);
211
212        let controller =
213            IntlController::new(Rc::clone(&storage_factory), setting_value_publisher).await;
214        let (tx, rx) = mpsc::unbounded();
215        controller.handle(rx).await.detach();
216
217        let (response_tx, response_rx) = oneshot::channel();
218        tx.unbounded_send(Request::Set(
219            IntlInfo {
220                locales: Some(vec![LocaleId { id: "en-US".to_string() }]),
221                temperature_unit: None,
222                time_zone_id: None,
223                hour_cycle: None,
224            },
225            response_tx,
226        ))
227        .expect("can send");
228
229        response_rx.await.expect("can receive").expect("should succeed");
230        let storage = storage_factory.get_device_storage().await;
231        let info = storage.get::<IntlInfo>().await;
232
233        assert_eq!(info.locales, Some(vec![LocaleId { id: "en-US".to_string() }]));
234        assert_eq!(info.temperature_unit, Some(TemperatureUnit::Celsius));
235        assert_eq!(info.time_zone_id, Some("UTC".to_string()));
236        assert_eq!(info.hour_cycle, Some(HourCycle::H12));
237    }
238}