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 super::intl_fidl_handler::Publisher;
6use crate::base::Merge;
7use crate::intl::types::{HourCycle, IntlInfo, LocaleId, TemperatureUnit};
8use anyhow::Error;
9use futures::StreamExt;
10use futures::channel::mpsc::UnboundedReceiver;
11use futures::channel::oneshot::Sender;
12use settings_common::inspect::event::{ResponseType, SettingValuePublisher};
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.into()));
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(
179                                "locale id",
180                                locale.id.clone().into(),
181                            ));
182                        }
183                    }
184                    Err(err) => {
185                        log::error!("Error loading locale: {:?}", err);
186                        return Err(IntlError::InvalidArgument(
187                            "locale id",
188                            locale.id.clone().into(),
189                        ));
190                    }
191                }
192            }
193        }
194
195        Ok(())
196    }
197
198    pub(crate) async fn restore(&self) -> IntlInfo {
199        self.store.get::<IntlInfo>().await
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use futures::channel::{mpsc, oneshot};
207    use settings_test_common::storage::InMemoryStorageFactory;
208
209    #[fuchsia::test]
210    async fn set_one() {
211        let storage_factory = InMemoryStorageFactory::new();
212        storage_factory.initialize::<IntlController>().await.expect("should initialize storage");
213        let storage_factory = Rc::new(storage_factory);
214
215        let (tx, _rx) = mpsc::unbounded();
216        let setting_value_publisher = SettingValuePublisher::new(tx);
217
218        let controller =
219            IntlController::new(Rc::clone(&storage_factory), setting_value_publisher).await;
220        let (tx, rx) = mpsc::unbounded();
221        controller.handle(rx).await.detach();
222
223        let (response_tx, response_rx) = oneshot::channel();
224        tx.unbounded_send(Request::Set(
225            IntlInfo {
226                locales: Some(vec![LocaleId { id: "en-US".to_string() }]),
227                temperature_unit: None,
228                time_zone_id: None,
229                hour_cycle: None,
230            },
231            response_tx,
232        ))
233        .expect("can send");
234
235        response_rx.await.expect("can receive").expect("should succeed");
236        let storage = storage_factory.get_device_storage().await;
237        let info = storage.get::<IntlInfo>().await;
238
239        assert_eq!(info.locales, Some(vec![LocaleId { id: "en-US".to_string() }]));
240        assert_eq!(info.temperature_unit, Some(TemperatureUnit::Celsius));
241        assert_eq!(info.time_zone_id, Some("UTC".to_string()));
242        assert_eq!(info.hour_cycle, Some(HourCycle::H12));
243    }
244}