settings/intl/
intl_controller.rs1use 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 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
87impl 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 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 fn validate_intl_info(&self, info: &IntlInfo) -> Result<(), IntlError> {
162 if let Some(time_zone_id) = &info.time_zone_id {
163 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 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}