settings/intl/
intl_controller.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use crate::base::{Merge, SettingInfo, SettingType};
use crate::handler::base::Request;
use crate::handler::setting_handler::persist::{controller as data_controller, ClientProxy};
use crate::handler::setting_handler::{
    controller, ControllerError, IntoHandlerResult, SettingHandlerResult,
};
use crate::intl::types::{HourCycle, IntlInfo, LocaleId, TemperatureUnit};
use async_trait::async_trait;
use settings_storage::device_storage::{DeviceStorage, DeviceStorageCompatible};
use settings_storage::fidl_storage::FidlStorageConvertible;
use settings_storage::storage_factory::{NoneT, StorageAccess};
use std::collections::HashSet;
use {fuchsia_trace as ftrace, rust_icu_uenum as uenum, rust_icu_uloc as uloc};

impl DeviceStorageCompatible for IntlInfo {
    type Loader = NoneT;
    const KEY: &'static str = "intl_info";
}

impl FidlStorageConvertible for IntlInfo {
    type Storable = fidl_fuchsia_settings::IntlSettings;
    type Loader = NoneT;
    const KEY: &'static str = "intl";

    fn to_storable(self) -> Self::Storable {
        self.into()
    }

    fn from_storable(storable: Self::Storable) -> Self {
        storable.into()
    }
}

impl Default for IntlInfo {
    fn default() -> Self {
        IntlInfo {
            // `-x-fxdef` is a private use extension and a special marker denoting that the
            // setting is a fallback default, and not actually set through any user action.
            locales: Some(vec![LocaleId { id: "en-US-x-fxdef".to_string() }]),
            temperature_unit: Some(TemperatureUnit::Celsius),
            time_zone_id: Some("UTC".to_string()),
            hour_cycle: Some(HourCycle::H12),
        }
    }
}

impl From<IntlInfo> for SettingInfo {
    fn from(info: IntlInfo) -> SettingInfo {
        SettingInfo::Intl(info)
    }
}

pub struct IntlController {
    client: ClientProxy,
    time_zone_ids: std::collections::HashSet<String>,
}

impl StorageAccess for IntlController {
    type Storage = DeviceStorage;
    type Data = IntlInfo;
    const STORAGE_KEY: &'static str = <IntlInfo as DeviceStorageCompatible>::KEY;
}

#[async_trait(?Send)]
impl data_controller::Create for IntlController {
    async fn create(client: ClientProxy) -> Result<Self, ControllerError> {
        let time_zone_ids = IntlController::load_time_zones();
        Ok(IntlController { client, time_zone_ids })
    }
}

#[async_trait(?Send)]
impl controller::Handle for IntlController {
    async fn handle(&self, request: Request) -> Option<SettingHandlerResult> {
        match request {
            Request::SetIntlInfo(info) => Some(self.set(info).await),
            Request::Get => Some(
                self.client
                    .read_setting_info::<IntlInfo>(ftrace::Id::new())
                    .await
                    .into_handler_result(),
            ),
            _ => None,
        }
    }
}

/// Controller for processing requests surrounding the Intl protocol, backed by a number of
/// services, including TimeZone.
impl IntlController {
    /// Loads the set of valid time zones from resources.
    fn load_time_zones() -> std::collections::HashSet<String> {
        let _icu_data_loader = icu_data::Loader::new().expect("icu data loaded");

        let time_zone_list = match uenum::open_time_zones() {
            Ok(time_zones) => time_zones,
            Err(err) => {
                tracing::error!("Unable to load time zones: {:?}", err);
                return HashSet::new();
            }
        };

        time_zone_list.flatten().collect()
    }

    async fn set(&self, info: IntlInfo) -> SettingHandlerResult {
        self.validate_intl_info(info.clone())?;

        let id = ftrace::Id::new();
        let current = self.client.read_setting::<IntlInfo>(id).await;
        self.client.write_setting(current.merge(info).into(), id).await.into_handler_result()
    }

    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/42069089)
    /// Checks if the given IntlInfo is valid.
    fn validate_intl_info(&self, info: IntlInfo) -> Result<(), ControllerError> {
        if let Some(time_zone_id) = info.time_zone_id {
            // Make sure the given time zone ID is valid.
            if !self.time_zone_ids.contains(time_zone_id.as_str()) {
                return Err(ControllerError::InvalidArgument(
                    SettingType::Intl,
                    "timezone id".into(),
                    time_zone_id.into(),
                ));
            }
        }

        if let Some(time_zone_locale) = info.locales {
            for locale in time_zone_locale {
                // NB: `try_from` doesn't actually do validation, `for_language_tag` does but doesn't
                // actually generate an error, it just ends up falling back to an empty string.
                let loc = uloc::ULoc::for_language_tag(locale.id.as_str());
                match loc {
                    Ok(parsed) => {
                        if parsed.label().is_empty() {
                            tracing::error!("Locale is invalid: {:?}", locale.id);
                            return Err(ControllerError::InvalidArgument(
                                SettingType::Intl,
                                "locale id".into(),
                                locale.id.into(),
                            ));
                        }
                    }
                    Err(err) => {
                        tracing::error!("Error loading locale: {:?}", err);
                        return Err(ControllerError::InvalidArgument(
                            SettingType::Intl,
                            "locale id".into(),
                            locale.id.into(),
                        ));
                    }
                }
            }
        }

        Ok(())
    }
}