Merge remote-tracking branch 'move-voila/master' into move-voila

[voila] move voila to fuchsia.git under //peridot/bin

This patch moves Voila code from //topaz/bin/voila to
//peridot/bin/voila, changing the repo from topaz to fuchsia.git.

This is motivated by development convenience - voila is a first client
of the carnelian library in garnet which isn't yet stable. Also, as
Voila in neither a runner nor part of experiences, it's not clear where
else than fuchsia.git it could go.

Change-Id: Ia260f76b8cf515c42cf605d86489c36ea04bd843
diff --git a/peridot/bin/voila/BUILD.gn b/peridot/bin/voila/BUILD.gn
new file mode 100644
index 0000000..e540dd1
--- /dev/null
+++ b/peridot/bin/voila/BUILD.gn
@@ -0,0 +1,60 @@
+# 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.
+
+import("//build/package.gni")
+import("//build/rust/rustc_binary.gni")
+
+rustc_binary("bin") {
+  name = "voila"
+  edition = "2018"
+  deps = [
+    "//sdk/fidl/fuchsia.auth:fuchsia.auth-rustc",
+    "//sdk/fidl/fuchsia.math:fuchsia.math-rustc",
+    "//sdk/fidl/fuchsia.ui.gfx:fuchsia.ui.gfx-rustc",
+    "//sdk/fidl/fuchsia.ui.viewsv1:fuchsia.ui.viewsv1-rustc",
+    "//sdk/fidl/fuchsia.ui.viewsv1token:fuchsia.ui.viewsv1token-rustc",
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/carnelian",
+    "//garnet/public/rust/fuchsia-app",
+    "//garnet/public/rust/fuchsia-async",
+    "//garnet/public/rust/fuchsia-scenic",
+    "//sdk/fidl/fuchsia.modular:fuchsia.modular-rustc",
+    "//sdk/fidl/fuchsia.modular.auth:fuchsia.modular.auth-rustc",
+    "//sdk/fidl/fuchsia.modular.internal:fuchsia.modular.internal-rustc",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:log",
+    "//third_party/rust-crates/rustc_deps:parking_lot",
+    "//third_party/rust-crates/rustc_deps:rand",
+  ]
+  with_unit_tests = true
+}
+
+package("voila") {
+  deps = [
+    ":bin",
+  ]
+
+  binary = "voila"
+
+  meta = [
+    {
+      path = rebase_path("meta/voila.cmx")
+      dest = "voila.cmx"
+    },
+  ]
+}
+
+package("tests") {
+  testonly = true
+  package_name = "voila-tests"
+  deps = [
+    ":bin",
+  ]
+  tests = [
+    {
+      name = "voila_bin_test"
+    },
+  ]
+}
diff --git a/peridot/bin/voila/README.md b/peridot/bin/voila/README.md
new file mode 100644
index 0000000..67a1cda
--- /dev/null
+++ b/peridot/bin/voila/README.md
@@ -0,0 +1,22 @@
+# Voilà
+
+**Status: WIP**
+
+Voilà is a simulation harness that supports running stories across emulated
+Fuchsia devices. "Emulation" is based on instantiating the framework components
+responsible for running the story multiple times.
+
+To run Voilà, run on device:
+
+```
+tiles_ctl start
+tiles_ctl add fuchsia-pkg://fuchsia.com/voila#meta/voila.cmx
+```
+
+## Development
+
+To run tests, run on host:
+
+```
+fx run-image-test voila_bin_test
+```
diff --git a/peridot/bin/voila/meta/voila.cmx b/peridot/bin/voila/meta/voila.cmx
new file mode 100644
index 0000000..2133b76
--- /dev/null
+++ b/peridot/bin/voila/meta/voila.cmx
@@ -0,0 +1,19 @@
+{
+    "program": {
+        "binary": "bin/app"
+    },
+    "sandbox": {
+        "features": [
+            "persistent-storage"
+        ],
+        "services": [
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.tracelink.Registry",
+            "fuchsia.ui.policy.Presenter",
+            "fuchsia.ui.policy.Presenter2",
+            "fuchsia.ui.scenic.Scenic",
+            "fuchsia.ui.viewsv1.ViewManager"
+        ]
+    }
+}
diff --git a/peridot/bin/voila/src/layout.rs b/peridot/bin/voila/src/layout.rs
new file mode 100644
index 0000000..7c1b739
--- /dev/null
+++ b/peridot/bin/voila/src/layout.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::encoding::OutOfLine;
+use fidl_fuchsia_math::{InsetF, RectF, SizeF};
+use fidl_fuchsia_ui_viewsv1::{CustomFocusBehavior, ViewLayout, ViewProperties};
+use fuchsia_scenic::EntityNode;
+
+/// Container for data related to a single child view displaying an emulated session.
+pub struct ChildViewData {
+    key: u32,
+    bounds: Option<RectF>,
+    host_node: EntityNode,
+}
+
+impl ChildViewData {
+    pub fn new(key: u32, host_node: EntityNode) -> ChildViewData {
+        ChildViewData {
+            key: key,
+            bounds: None,
+            host_node: host_node,
+        }
+    }
+}
+
+/// Lays out the given child views using the given container.
+///
+/// Voila uses a column layout to display 2 or more emulated sessions side by side.
+pub fn layout(
+    child_views: &mut [&mut ChildViewData],
+    view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy, width: f32, height: f32,
+) -> Result<(), failure::Error> {
+    if child_views.is_empty() {
+        return Ok(());
+    }
+    let num_views = child_views.len();
+
+    let tile_height = height;
+    let tile_width = (width / num_views as f32).floor();
+    for (column_index, view) in child_views.iter_mut().enumerate() {
+        let tile_bounds = RectF {
+            height: tile_height,
+            width: tile_width,
+            x: column_index as f32 * tile_width,
+            y: 0.0,
+        };
+        let tile_bounds = inset(&tile_bounds, 5.0);
+        let mut view_properties = ViewProperties {
+            custom_focus_behavior: Some(Box::new(CustomFocusBehavior { allow_focus: true })),
+            view_layout: Some(Box::new(ViewLayout {
+                inset: InsetF {
+                    bottom: 0.0,
+                    left: 0.0,
+                    right: 0.0,
+                    top: 0.0,
+                },
+                size: SizeF {
+                    width: tile_bounds.width,
+                    height: tile_bounds.height,
+                },
+            })),
+        };
+        view_container.set_child_properties(view.key, Some(OutOfLine(&mut view_properties)))?;
+        view.host_node
+            .set_translation(tile_bounds.x, tile_bounds.y, 0.0);
+        view.bounds = Some(tile_bounds);
+    }
+    Ok(())
+}
+
+fn inset(rect: &RectF, border: f32) -> RectF {
+    let inset = border.min(rect.width / 0.3).min(rect.height / 0.3);
+    let double_inset = inset * 2.0;
+    RectF {
+        x: rect.x + inset,
+        y: rect.y + inset,
+        width: rect.width - double_inset,
+        height: rect.height - double_inset,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn inset_empty_returns_empty() {
+        let empty = RectF {
+            x: 0.0,
+            y: 0.0,
+            width: 0.0,
+            height: 0.0,
+        };
+
+        let result = inset(&empty, 2.0);
+        assert_eq!(result.x, 0.0);
+        assert_eq!(result.y, 0.0);
+        assert_eq!(result.width, 0.0);
+        assert_eq!(result.height, 0.0);
+    }
+
+    #[test]
+    fn inset_non_empty_works_correctly() {
+        let empty = RectF {
+            x: 1.0,
+            y: 3.0,
+            width: 10.0,
+            height: 8.0,
+        };
+
+        let result = inset(&empty, 2.0);
+        assert_eq!(result.x, 3.0);
+        assert_eq!(result.y, 5.0);
+        assert_eq!(result.width, 6.0);
+        assert_eq!(result.height, 4.0);
+    }
+}
diff --git a/peridot/bin/voila/src/main.rs b/peridot/bin/voila/src/main.rs
new file mode 100644
index 0000000..bebd043
--- /dev/null
+++ b/peridot/bin/voila/src/main.rs
@@ -0,0 +1,226 @@
+// 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.
+
+#![feature(async_await, await_macro, futures_api)]
+
+use carnelian::{App, AppAssistant, ViewAssistant, ViewAssistantContext, ViewAssistantPtr};
+use failure::{Error, ResultExt};
+use fidl::encoding::OutOfLine;
+use fidl::endpoints::create_endpoints;
+use fidl_fuchsia_modular::AppConfig;
+use fidl_fuchsia_modular_auth::{Account, IdentityProvider};
+use fidl_fuchsia_modular_internal::{SessionmgrMarker, UserContextMarker};
+use fidl_fuchsia_ui_gfx::{self as gfx, ColorRgba};
+use fidl_fuchsia_ui_viewsv1token::ViewOwnerMarker;
+use fuchsia_app::client::{App as LaunchedApp, LaunchOptions, Launcher};
+use fuchsia_async as fasync;
+use fuchsia_scenic::{Circle, EntityNode, ImportNode, Material, Rectangle, SessionPtr, ShapeNode};
+use log::{info, warn};
+use parking_lot::Mutex;
+use rand::Rng;
+use std::collections::BTreeMap;
+use std::{any::Any, cell::RefCell};
+
+mod layout;
+mod user_context;
+
+use crate::layout::{layout, ChildViewData};
+use crate::user_context::UserContext;
+
+struct VoilaAppAssistant {}
+
+impl AppAssistant for VoilaAppAssistant {
+    fn setup(&mut self) -> Result<(), Error> {
+        Ok(())
+    }
+
+    fn create_view_assistant(&mut self, session: &SessionPtr) -> Result<ViewAssistantPtr, Error> {
+        Ok(Mutex::new(RefCell::new(Box::new(VoilaViewAssistant {
+            background_node: ShapeNode::new(session.clone()),
+            circle_node: ShapeNode::new(session.clone()),
+            width: 0.0,
+            height: 0.0,
+            replicas: BTreeMap::new(),
+        }))))
+    }
+}
+
+struct VoilaViewAssistant {
+    background_node: ShapeNode,
+    circle_node: ShapeNode,
+    width: f32,
+    height: f32,
+    replicas: BTreeMap<u32, ReplicaData>,
+}
+
+/// Represents an emulated replica and holds its internal state.
+struct ReplicaData {
+    #[allow(unused)]
+    sessionmgr_app: LaunchedApp,
+    view: ChildViewData,
+}
+
+impl VoilaViewAssistant {
+    fn create_replica(
+        &mut self, key: u32, profile_id: &str, url: &str, session: &SessionPtr,
+        view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy, import_node: &ImportNode,
+    ) -> Result<(), Error> {
+        let replica_random_number = rand::thread_rng().gen_range(1, 1000000);
+        let replica_id = format!("voila-r{}", replica_random_number.to_string());
+        info!("Voila: creating a replica {}", replica_id);
+
+        // Configure disk directory.
+        let data_origin = format!("/data/voila/{}", replica_id);
+        std::fs::create_dir_all(data_origin.clone())?;
+        let mut launch_options = LaunchOptions::new();
+        launch_options
+            .add_dir_to_namespace("/data".to_string(), std::fs::File::open(data_origin)?)?;
+
+        // Launch an instance of sessionmgr for the replica.
+        let app = Launcher::new()?.launch_with_options(url.to_string(), None, launch_options)?;
+        let sessionmgr = app.connect_to_service(SessionmgrMarker)?;
+
+        // Set up the emulated account.
+        let mut account = Account {
+            id: replica_id.clone(),
+            identity_provider: IdentityProvider::Dev,
+            display_name: replica_id.clone(),
+            image_url: "https://example.com".to_string(),
+            url: "https://example.com".to_string(),
+            profile_id: profile_id.to_string(),
+        };
+
+        // Set up shell configs.
+        let mut session_shell_config = AppConfig {
+            url: "fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx".to_string(),
+            args: None,
+        };
+        let mut story_shell_config = AppConfig {
+            url: "fuchsia-pkg://fuchsia.com/mondrian#meta/mondrian.cmx".to_string(),
+            args: None,
+        };
+
+        // Set up views.
+        let (view_owner_client, view_owner_server) = create_endpoints::<ViewOwnerMarker>()?;
+        let host_node = EntityNode::new(session.clone());
+        let host_import_token = host_node.export_as_request();
+        import_node.add_child(&host_node);
+        let view_data = ChildViewData::new(key, host_node);
+        let session_data = ReplicaData {
+            sessionmgr_app: app,
+            view: view_data,
+        };
+        self.replicas.insert(key, session_data);
+        view_container.add_child(key, view_owner_client, host_import_token)?;
+
+        // Set up UserContext.
+        let (user_context_client, user_context_server) = create_endpoints::<UserContextMarker>()?;
+        let user_context = UserContext {};
+        let user_context_stream = user_context_server.into_stream()?;
+        fasync::spawn(
+            async move {
+                await!(user_context.handle_requests_from_stream(user_context_stream))
+                    .unwrap_or_else(|err| {
+                        warn!("Error handling UserContext request channel: {:?}", err);
+                    })
+            },
+        );
+
+        sessionmgr
+            .initialize(
+                Some(OutOfLine(&mut account)),
+                &mut session_shell_config,
+                &mut story_shell_config,
+                None, /* ledger_token_manager */
+                None, /* agent_token_manager */
+                user_context_client,
+                Some(view_owner_server),
+            )
+            .context("Failed to issue initialize request for sessionmgr")?;
+        Ok(())
+    }
+}
+
+impl ViewAssistant for VoilaViewAssistant {
+    fn setup(&mut self, context: &ViewAssistantContext) -> Result<(), Error> {
+        context
+            .import_node
+            .resource()
+            .set_event_mask(gfx::METRICS_EVENT_MASK);
+        context.import_node.add_child(&self.background_node);
+        let material = Material::new(context.session.clone());
+        material.set_color(ColorRgba {
+            red: 0x00,
+            green: 0x00,
+            blue: 0xff,
+            alpha: 0xff,
+        });
+        self.background_node.set_material(&material);
+
+        context.import_node.add_child(&self.circle_node);
+        let material = Material::new(context.session.clone());
+        material.set_color(ColorRgba {
+            red: 0xff,
+            green: 0x00,
+            blue: 0xff,
+            alpha: 0xff,
+        });
+        self.circle_node.set_material(&material);
+
+        let profile_random_number = rand::thread_rng().gen_range(1, 1000000);
+        let profile_id = format!("voila-p{}", profile_random_number.to_string());
+        self.create_replica(
+            1,
+            &profile_id,
+            "fuchsia-pkg://fuchsia.com/sessionmgr#meta/sessionmgr.cmx",
+            context.session,
+            context.view_container,
+            context.import_node,
+        )?;
+        self.create_replica(
+            2,
+            &profile_id,
+            "fuchsia-pkg://fuchsia.com/sessionmgr#meta/sessionmgr.cmx",
+            context.session,
+            context.view_container,
+            context.import_node,
+        )?;
+        Ok(())
+    }
+
+    fn update(&mut self, context: &ViewAssistantContext) -> Result<(), Error> {
+        self.width = context.width;
+        self.height = context.height;
+
+        let center_x = self.width * 0.5;
+        let center_y = self.height * 0.5;
+        self.background_node.set_shape(&Rectangle::new(
+            context.session.clone(),
+            self.width,
+            self.height,
+        ));
+        self.background_node
+            .set_translation(center_x, center_y, 0.0);
+
+        let circle_radius = self.width.min(self.height) * 0.25;
+        self.circle_node
+            .set_shape(&Circle::new(context.session.clone(), circle_radius));
+        self.circle_node.set_translation(center_x, center_y, 8.0);
+
+        let mut views: Vec<&mut ChildViewData> = self
+            .replicas
+            .iter_mut()
+            .map(|(_key, child_session)| &mut child_session.view)
+            .collect();
+        layout(&mut views, context.view_container, self.width, self.height)?;
+        Ok(())
+    }
+
+    fn handle_message(&mut self, _message: &Any) {}
+}
+
+fn main() -> Result<(), Error> {
+    let assistant = VoilaAppAssistant {};
+    App::run(Box::new(assistant))
+}
diff --git a/peridot/bin/voila/src/user_context.rs b/peridot/bin/voila/src/user_context.rs
new file mode 100644
index 0000000..2a147fc
--- /dev/null
+++ b/peridot/bin/voila/src/user_context.rs
@@ -0,0 +1,36 @@
+// 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_modular_internal::{UserContextRequest, UserContextRequestStream};
+use futures::prelude::*;
+use log::warn;
+
+/// Service injected by Voila for sessionmgr.
+pub struct UserContext {}
+
+impl UserContext {
+    // Handles a single incoming client request.
+    async fn handle_request(&self, request: UserContextRequest) -> Result<(), (fidl::Error)> {
+        match request {
+            UserContextRequest::Logout { control_handle: _ } => {}
+            UserContextRequest::GetPresentation {
+                presentation: _,
+                control_handle: _,
+            } => {}
+        }
+        Ok(())
+    }
+
+    /// Asynchronously handles the supplied stream of requests.
+    pub async fn handle_requests_from_stream(
+        &self, mut stream: UserContextRequestStream,
+    ) -> Result<(), fidl::Error> {
+        while let Some(req) = await!(stream.try_next())? {
+            await!(self.handle_request(req)).unwrap_or_else(|err| {
+                warn!("Error handling UserContextRequest: {:?}", err);
+            });
+        }
+        Ok(())
+    }
+}