From 145eeba78a64853be4db9f8d661eed8fac5997d9 Mon Sep 17 00:00:00 2001
From: Bryce Lee <brycelee@google.com>
Date: Sat, 27 Apr 2019 07:45:37 +0000
Subject: [PATCH] [SetUI] Introduce Adapters.

This changelist adds adapters to the SetUI setting service. Adapters
allow us to extend the service to handle different types of settings. It
also implements the setting listen functionality.

A TestAdapter has also been added for the Unknown setting type as
an example.

Bug: SU-167
Test: fx run-test setui_service_tests -- --test
Change-Id: Ia105d77a18ba80d3624bb45d509c6ad6b211a5c3
---
 garnet/bin/setui/src/common.rs          |  25 ++++
 garnet/bin/setui/src/fidl_clone.rs      | 118 +++++++++++++++++++
 garnet/bin/setui/src/main.rs            |  13 +++
 garnet/bin/setui/src/mutation.rs        |  17 +++
 garnet/bin/setui/src/setting_adapter.rs |  80 +++++++++++++
 garnet/bin/setui/src/setui_handler.rs   | 145 +++++++++++++++++++++---
 6 files changed, 384 insertions(+), 14 deletions(-)
 create mode 100644 garnet/bin/setui/src/common.rs
 create mode 100644 garnet/bin/setui/src/fidl_clone.rs
 create mode 100644 garnet/bin/setui/src/mutation.rs
 create mode 100644 garnet/bin/setui/src/setting_adapter.rs

diff --git a/garnet/bin/setui/src/common.rs b/garnet/bin/setui/src/common.rs
new file mode 100644
index 00000000000..e6c60f9967d
--- /dev/null
+++ b/garnet/bin/setui/src/common.rs
@@ -0,0 +1,25 @@
+// 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 failure::Error;
+use fidl_fuchsia_setui::*;
+use std::sync::mpsc::Sender;
+
+pub type ProcessMutation = dyn Fn(&Mutation) -> Result<Option<SettingData>, Error> + Send + Sync;
+
+/// Trait defining interface setting service uses to relay operations. Each
+/// adapter specifies the setting type it handles, which it will accept
+/// mutations for and relay updates.
+pub trait Adapter {
+    /// Returns the setting type this adapter is responsible for handling.
+    fn get_type(&self) -> SettingType;
+
+    /// Applies a mutation on the given adapter.
+    fn mutate(&mut self, mutation: &fidl_fuchsia_setui::Mutation) -> MutationResponse;
+
+    /// Registers a listener. The current value known to the client is passed
+    /// along. If an updated value is known, the sender is immediately invoked.
+    /// Otherwise, the sender is stored for later invocation.
+    fn listen(&self, sender: Sender<SettingData>, last_seen_data: Option<&SettingData>);
+}
diff --git a/garnet/bin/setui/src/fidl_clone.rs b/garnet/bin/setui/src/fidl_clone.rs
new file mode 100644
index 00000000000..4d80df338d3
--- /dev/null
+++ b/garnet/bin/setui/src/fidl_clone.rs
@@ -0,0 +1,118 @@
+// 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 fidl_fuchsia_setui::*;
+
+/// A placeholder for real cloning support in FIDL generated Rust code.
+/// TODO(QA-715): Remove
+pub trait FIDLClone {
+    fn clone(&self) -> Self;
+}
+
+impl FIDLClone for SettingData {
+    fn clone(&self) -> SettingData {
+        match self {
+            SettingData::StringValue(val) => {
+                return SettingData::StringValue(val.to_string());
+            }
+            SettingData::TimeZoneValue(val) => {
+                return SettingData::TimeZoneValue(val.clone());
+            }
+            SettingData::Connectivity(val) => {
+                return SettingData::Connectivity(val.clone());
+            }
+            SettingData::Intl(val) => {
+                return SettingData::Intl(val.clone());
+            }
+            SettingData::Wireless(val) => {
+                return SettingData::Wireless(val.clone());
+            }
+            SettingData::Account(val) => {
+                return SettingData::Account(val.clone());
+            }
+        }
+    }
+}
+
+impl FIDLClone for AccountSettings {
+    fn clone(&self) -> Self {
+        return AccountSettings { mode: self.mode };
+    }
+}
+
+impl FIDLClone for IntlSettings {
+    fn clone(&self) -> Self {
+        return IntlSettings {
+            locales: self.locales.clone(),
+            hour_cycle: self.hour_cycle,
+            temperature_unit: self.temperature_unit,
+        };
+    }
+}
+
+impl FIDLClone for ConnectedState {
+    fn clone(&self) -> ConnectedState {
+        return ConnectedState { reachability: self.reachability };
+    }
+}
+
+impl<T: FIDLClone> FIDLClone for Vec<T> {
+    fn clone(&self) -> Self {
+        return self.into_iter().map(FIDLClone::clone).collect();
+    }
+}
+
+impl<T: FIDLClone> FIDLClone for Option<Box<T>> {
+    fn clone(&self) -> Self {
+        match self {
+            None => None,
+            Some(val) => Some(Box::new(val.as_ref().clone())),
+        }
+    }
+}
+
+impl FIDLClone for TimeZoneInfo {
+    fn clone(&self) -> TimeZoneInfo {
+        return TimeZoneInfo { current: self.current.clone(), available: self.available.clone() };
+    }
+}
+
+impl FIDLClone for TimeZone {
+    fn clone(&self) -> Self {
+        return TimeZone {
+            id: self.id.clone(),
+            name: self.name.clone(),
+            region: self.region.clone(),
+        };
+    }
+}
+
+impl FIDLClone for WirelessState {
+    fn clone(&self) -> Self {
+        return WirelessState { wireless_networks: self.wireless_networks.clone() };
+    }
+}
+
+impl FIDLClone for WirelessNetwork {
+    fn clone(&self) -> Self {
+        return WirelessNetwork {
+            internal_id: self.internal_id,
+            ssid: self.ssid.clone(),
+            wpa_auth: self.wpa_auth,
+            wpa_cipher: self.wpa_cipher,
+            access_points: self.access_points.clone(),
+        };
+    }
+}
+
+impl FIDLClone for WirelessAccessPoint {
+    fn clone(&self) -> Self {
+        return WirelessAccessPoint {
+            bssid: self.bssid.clone(),
+            frequency: self.frequency,
+            rssi: self.rssi,
+            status: self.status,
+        };
+    }
+}
diff --git a/garnet/bin/setui/src/main.rs b/garnet/bin/setui/src/main.rs
index fbed5784a12..ce0163714fe 100644
--- a/garnet/bin/setui/src/main.rs
+++ b/garnet/bin/setui/src/main.rs
@@ -4,6 +4,8 @@
 #![feature(async_await, await_macro, futures_api)]
 
 use {
+    crate::mutation::*,
+    crate::setting_adapter::SettingAdapter,
     failure::Error,
     fidl_fuchsia_setui::*,
     fuchsia_async as fasync,
@@ -15,6 +17,10 @@ use {
     std::sync::Arc,
 };
 
+mod common;
+mod fidl_clone;
+mod mutation;
+mod setting_adapter;
 mod setui_handler;
 
 fn main() -> Result<(), Error> {
@@ -26,6 +32,13 @@ fn main() -> Result<(), Error> {
     let mut fs = ServiceFs::new();
     let handler = Arc::new(SetUIHandler::new());
 
+    // TODO(SU-210): Remove once other adapters are ready.
+    handler.register_adapter(Box::new(SettingAdapter::new(
+        SettingType::Unknown,
+        Box::new(process_string_mutation),
+        None,
+    )));
+
     fs.dir("public").add_fidl_service(move |stream: SetUiServiceRequestStream| {
         let handler_clone = handler.clone();
         fx_log_info!("Connecting to setui_service");
diff --git a/garnet/bin/setui/src/mutation.rs b/garnet/bin/setui/src/mutation.rs
new file mode 100644
index 00000000000..0eee85710e2
--- /dev/null
+++ b/garnet/bin/setui/src/mutation.rs
@@ -0,0 +1,17 @@
+// 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 failure::{format_err, Error};
+use fidl_fuchsia_setui::*;
+
+pub fn process_string_mutation(mutation: &Mutation) -> Result<Option<SettingData>, Error> {
+    if let Mutation::StringMutationValue(mutation_info) = mutation {
+        if mutation_info.operation == StringOperation::Update {
+            return Ok(Some(SettingData::StringValue(mutation_info.value.clone())));
+        }
+
+        return Ok(None);
+    } else {
+        return Err(format_err!("invalid error"));
+    }
+}
diff --git a/garnet/bin/setui/src/setting_adapter.rs b/garnet/bin/setui/src/setting_adapter.rs
new file mode 100644
index 00000000000..a039d522215
--- /dev/null
+++ b/garnet/bin/setui/src/setting_adapter.rs
@@ -0,0 +1,80 @@
+// 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::common::*;
+use crate::fidl_clone::*;
+use fidl_fuchsia_setui::*;
+use std::sync::mpsc::Sender;
+use std::sync::Mutex;
+
+/// SettingAdapter provides a basic implementation of the Adapter trait,
+/// handling callbacks for hanging get interactions and keeping track of the
+/// latest values. Users of this implementation can provide a callback for
+/// processing mutations.
+pub struct SettingAdapter {
+    setting_type: SettingType,
+    latest_val: Option<SettingData>,
+    senders: Mutex<Vec<Sender<SettingData>>>,
+    mutation_process: Box<ProcessMutation>,
+}
+
+impl SettingAdapter {
+    pub fn new(
+        setting_type: SettingType,
+        mutation_processor: Box<ProcessMutation>,
+        default_value: Option<SettingData>,
+    ) -> SettingAdapter {
+        return SettingAdapter {
+            setting_type: setting_type,
+            latest_val: default_value,
+            senders: Mutex::new(vec![]),
+            mutation_process: mutation_processor,
+        };
+    }
+}
+
+impl Adapter for SettingAdapter {
+    fn get_type(&self) -> SettingType {
+        return self.setting_type;
+    }
+
+    fn mutate(&mut self, mutation: &fidl_fuchsia_setui::Mutation) -> MutationResponse {
+        let result = (self.mutation_process)(mutation);
+
+        match result {
+            Ok(Some(setting)) => {
+                self.latest_val = Some(setting.clone());
+
+                if let Ok(mut senders) = self.senders.lock() {
+                    while !senders.is_empty() {
+                        let sender_option = senders.pop();
+
+                        if let Some(sender) = sender_option {
+                            sender.send(setting.clone()).ok();
+                        }
+                    }
+                }
+            }
+            Ok(None) => {
+                self.latest_val = None;
+            }
+            _ => {
+                return MutationResponse { return_code: ReturnCode::Failed };
+            }
+        }
+
+        return MutationResponse { return_code: ReturnCode::Ok };
+    }
+
+    /// Listen
+    fn listen(&self, sender: Sender<SettingData>, _last_seen_data: Option<&SettingData>) {
+        if let Some(ref value) = self.latest_val {
+            sender.send(value.clone()).ok();
+        } else {
+            if let Ok(mut senders) = self.senders.lock() {
+                senders.push(sender);
+            }
+        }
+    }
+}
diff --git a/garnet/bin/setui/src/setui_handler.rs b/garnet/bin/setui/src/setui_handler.rs
index 179b556747d..506f276eb5f 100644
--- a/garnet/bin/setui/src/setui_handler.rs
+++ b/garnet/bin/setui/src/setui_handler.rs
@@ -2,44 +2,112 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use failure::Error;
+use crate::common::Adapter;
+use crate::fidl_clone::*;
+use failure::{format_err, Error};
 use fidl_fuchsia_setui::*;
+use fuchsia_syslog::fx_log_err;
 use futures::prelude::*;
+use std::collections::HashMap;
+use std::sync::mpsc::{channel, Sender};
+use std::sync::Arc;
+use std::sync::Mutex;
+use std::sync::RwLock;
 
-pub struct SetUIHandler {}
+use fuchsia_async as fasync;
+
+pub struct SetUIHandler {
+    // Tracks active adapters.
+    adapters: RwLock<HashMap<SettingType, Mutex<Box<dyn Adapter + Send + Sync>>>>,
+    // Provides bookkeeping for last value transmitted by a callback. Asynchronous
+    // to allow sharing between all callback closures.
+    last_seen_settings: Arc<RwLock<HashMap<SettingType, SettingData>>>,
+}
 
 /// SetUIHandler handles all API calls for the service. It is intended to be
 /// used as a single instance to service multiple streams.
 impl SetUIHandler {
-    /// In the future, will populate with supporting classes, such as adapters.
     pub fn new() -> SetUIHandler {
-        Self {}
+        Self {
+            adapters: RwLock::new(HashMap::new()),
+            last_seen_settings: Arc::new(RwLock::new(HashMap::new())),
+        }
+    }
+
+    /// Adds (or replaces) any existing adapter. Setting type mapping is
+    /// determined from the adapter's reported type.
+    pub fn register_adapter(&self, adapter: Box<dyn Adapter + Send + Sync>) {
+        self.adapters.write().unwrap().insert(adapter.get_type(), Mutex::new(adapter));
     }
 
     /// Asynchronous handling of the given stream. Note that we must consider the
     /// possibility of simultaneous active streams.
     pub async fn handle_stream(&self, mut stream: SetUiServiceRequestStream) -> Result<(), Error> {
+        // A single StreamSetting watcher is created for the lifetime of the stream.
+        // It is used for asynchronously processing updates to a setting type and
+        // tracking the current values.
+
         while let Some(req) = await!(stream.try_next())? {
-            await!(self.handle_request(req))?;
+            match req {
+                SetUiServiceRequest::Mutate { setting_type, mutation, responder } => {
+                    let mut response = self.mutate(setting_type, mutation);
+                    responder.send(&mut response)?;
+                }
+                SetUiServiceRequest::Watch { setting_type, responder } => {
+                    self.watch(setting_type, responder);
+                }
+                _ => {}
+            }
         }
 
         Ok(())
     }
 
-    /// Routes a given request to the proper handling function.
-    pub async fn handle_request(&self, req: SetUiServiceRequest) -> Result<(), fidl::Error> {
-        match req {
-            SetUiServiceRequest::Mutate { setting_type, mutation, responder } => {
-                let mut response = self.mutate(setting_type, mutation);
-                responder.send(&mut response)?;
+    fn watch(&self, setting_type: SettingType, responder: SetUiServiceWatchResponder) {
+        let (sender, receiver) = channel();
+
+        if self.listen(setting_type, sender).is_ok() {
+            // We clone here so the value can be moved into the closure below.
+            let last_seen_settings_clone = self.last_seen_settings.clone();
+
+            fasync::spawn(
+                async move {
+                    let data: SettingData = receiver.recv().unwrap();
+                    last_seen_settings_clone.write().unwrap().insert(setting_type, data.clone());
+                    responder
+                        .send(&mut SettingsObject {
+                            setting_type: setting_type,
+                            data: data.clone(),
+                        })
+                        .ok();
+                },
+            );
+        } else {
+            fx_log_err!("watch: no valid adapter for type");
+        }
+    }
+
+    /// The core logic behind listening to a setting is separate from the watch
+    /// method definition to facilitate testing.
+    fn listen(&self, setting_type: SettingType, sender: Sender<SettingData>) -> Result<(), Error> {
+        if let Some(adapter_lock) = self.adapters.read().unwrap().get(&setting_type) {
+            if let Ok(adapter) = adapter_lock.lock() {
+                adapter.listen(sender, self.last_seen_settings.read().unwrap().get(&setting_type));
+                return Ok(());
             }
-            _ => {}
         }
-        Ok(())
+
+        return Err(format_err!("cannot listen. non-existent adapter"));
     }
 
     /// Applies a mutation
-    fn mutate(&self, _setting_type: SettingType, _mutation: Mutation) -> MutationResponse {
+    fn mutate(&self, setting_type: SettingType, mutation: Mutation) -> MutationResponse {
+        if let Some(adapter_lock) = self.adapters.read().unwrap().get(&setting_type) {
+            if let Ok(mut adapter) = adapter_lock.lock() {
+                adapter.mutate(&mutation);
+            }
+        }
+
         MutationResponse { return_code: ReturnCode::Ok }
     }
 }
@@ -47,6 +115,8 @@ impl SetUIHandler {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::mutation::*;
+    use crate::setting_adapter::SettingAdapter;
 
     /// A basic test to exercise that basic functionality works. In this case, we
     /// mutate the unknown type, reserved for testing. We should always immediately
@@ -65,4 +135,51 @@ mod tests {
         assert_eq!(result, MutationResponse { return_code: ReturnCode::Ok });
     }
 
+    /// A test to verify the listen functionality. Note that we use the listen
+    /// method directly on the handler as we do not mock the FIDL interface.
+    #[test]
+    fn test_listen() {
+        // Test value to ensure
+        let test_val = "FooBar".to_string();
+        // Create handler and register test adapter
+        let handler = SetUIHandler::new();
+        handler.register_adapter(Box::new(SettingAdapter::new(
+            SettingType::Unknown,
+            Box::new(process_string_mutation),
+            None,
+        )));
+
+        // Introduce change to the adapter.
+        assert_eq!(
+            handler.mutate(
+                SettingType::Unknown,
+                fidl_fuchsia_setui::Mutation::StringMutationValue(StringMutation {
+                    operation: StringOperation::Update,
+                    value: test_val.clone()
+                }),
+            ),
+            MutationResponse { return_code: ReturnCode::Ok }
+        );
+
+        let (sender, receiver) = channel();
+
+        // Listen for change
+        assert!(handler.listen(SettingType::Unknown, sender).is_ok());
+
+        let listen_result = receiver.recv();
+
+        assert!(listen_result.is_ok());
+
+        let data = listen_result.unwrap();
+
+        // Ensure value matches original change.
+        match data {
+            SettingData::StringValue(string_val) => {
+                assert_eq!(string_val, test_val.clone());
+            }
+            _ => {
+                panic!("Did not receive back expected SettingData type");
+            }
+        }
+    }
 }
-- 
GitLab