Revert "[base_shell] remove userpicker_base_shell, update ermine config"
This reverts commit f709c204e5f9f8e4a3b309c13a92aec5bc923109.
Reason for revert: breaks Ledger sync, which needs a signed in user
Original change's description:
> [base_shell] remove userpicker_base_shell, update ermine config
>
> Test: tried ermine
>
> Change-Id: Ib3c821a944fe411f701222f0a6eebc5bab53be0d
TBR=ejia@google.com,sanjayc@google.com,alexmin@google.com,brycelee@google.com
# Not skipping CQ checks because original CL landed > 1 day ago.
Change-Id: I38d88e3e08505b0879ab7229beb11d8b07b8f281
diff --git a/bin/userpicker_base_shell/BUILD.gn b/bin/userpicker_base_shell/BUILD.gn
new file mode 100644
index 0000000..e4b4125
--- /dev/null
+++ b/bin/userpicker_base_shell/BUILD.gn
@@ -0,0 +1,66 @@
+# Copyright 2017 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("//third_party/cobalt_config/metrics_registry.gni")
+import("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+metrics_registry("sysui_metrics_config") {
+ project_id = 103
+}
+
+flutter_app("userpicker_base_shell") {
+ main_dart = "lib/main.dart"
+
+ meta = [
+ {
+ path = rebase_path("meta/userpicker_base_shell.cmx")
+ dest = "userpicker_base_shell.cmx"
+ },
+ ]
+
+ resources = [
+ {
+ path = rebase_path(
+ get_label_info(":sysui_metrics_config", "target_gen_dir") +
+ "/sysui_metrics_config.pb")
+ dest = "sysui_metrics_config.pb"
+ },
+ ]
+
+ package_name = "userpicker_base_shell"
+
+ manifest = "pubspec.yaml"
+
+ sources = [
+ "authentication_overlay.dart",
+ "authentication_overlay_model.dart",
+ "authentication_ui_context_impl.dart",
+ "circular_button.dart",
+ "clock.dart",
+ "time_stringer.dart",
+ "user_list.dart",
+ "user_picker_base_shell_model.dart",
+ "user_picker_base_shell_screen.dart",
+ "user_picker_screen.dart",
+ ]
+
+ non_dart_deps = [ ":sysui_metrics_config" ]
+
+ deps = [
+ "//sdk/fidl/fuchsia.modular",
+ "//sdk/fidl/fuchsia.modular.auth",
+ "//sdk/fidl/fuchsia.timezone",
+ "//sdk/fidl/fuchsia.ui.input",
+ "//sdk/fidl/fuchsia.ui.views",
+ "//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//third_party/dart-pkg/pub/http",
+ "//topaz/lib/base_shell:lib.base_shell",
+ "//topaz/lib/settings:lib.settings",
+ "//topaz/public/dart/fuchsia_logger",
+ "//topaz/public/dart/fidl",
+ "//topaz/public/dart/fuchsia_scenic_flutter",
+ "//topaz/public/dart/widgets:lib.widgets",
+ "//topaz/public/lib/device/dart",
+ ]
+}
diff --git a/bin/userpicker_base_shell/README.md b/bin/userpicker_base_shell/README.md
new file mode 100644
index 0000000..76b12ff
--- /dev/null
+++ b/bin/userpicker_base_shell/README.md
@@ -0,0 +1,6 @@
+# User-picker Base Shell
+
+This directory contains an example implementation of base shell written in
+Flutter that can be used to choose from a list of users to login.
+
+This is started by basemgr unless overidden from command line.
diff --git a/bin/userpicker_base_shell/analysis_options.yaml b/bin/userpicker_base_shell/analysis_options.yaml
new file mode 100644
index 0000000..e688f26
--- /dev/null
+++ b/bin/userpicker_base_shell/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright 2017 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.
+
+include: ../analysis_options.yaml
diff --git a/bin/userpicker_base_shell/lib/authentication_overlay.dart b/bin/userpicker_base_shell/lib/authentication_overlay.dart
new file mode 100644
index 0000000..df554e9
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/authentication_overlay.dart
@@ -0,0 +1,60 @@
+// Copyright 2017 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 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:flutter/material.dart';
+import 'package:lib.widgets/model.dart';
+
+import 'authentication_overlay_model.dart';
+
+/// Signature for callback used to indicate that the user cancelled the authorization
+/// flow.
+typedef AuthenticationCancelCallback = void Function();
+
+/// Displays the authentication window.
+class AuthenticationOverlay extends StatelessWidget {
+ /// Constructs an authentication overlay that calls the provided callback if the
+ /// user cancels the login flow.
+ const AuthenticationOverlay({AuthenticationCancelCallback onCancel})
+ : _onCancel = onCancel;
+
+ /// The callback that is triggered when the user taps outside of the child view,
+ /// cancelling the authorization flow.
+ final AuthenticationCancelCallback _onCancel;
+
+ @override
+ Widget build(BuildContext context) =>
+ new ScopedModelDescendant<AuthenticationOverlayModel>(
+ builder: (
+ BuildContext context,
+ Widget child,
+ AuthenticationOverlayModel model,
+ ) =>
+ new AnimatedBuilder(
+ animation: model.animation,
+ builder: (BuildContext context, Widget child) => new Offstage(
+ offstage: model.animation.isDismissed,
+ child: new Opacity(
+ opacity: model.animation.value,
+ child: child,
+ ),
+ ),
+ child: new Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ new GestureDetector(
+ onTap: _onCancel,
+ ),
+ new FractionallySizedBox(
+ widthFactor: 0.75,
+ heightFactor: 0.75,
+ child: new ChildView(
+ connection: model.childViewConnection,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
diff --git a/bin/userpicker_base_shell/lib/authentication_overlay_model.dart b/bin/userpicker_base_shell/lib/authentication_overlay_model.dart
new file mode 100644
index 0000000..fe633c1
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/authentication_overlay_model.dart
@@ -0,0 +1,77 @@
+// Copyright 2017 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 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
+ show ChildViewConnection;
+import 'package:lib.widgets/model.dart';
+
+/// Manages the connection and animation of the authentication window.
+class AuthenticationOverlayModel extends Model implements TickerProvider {
+ ChildViewConnection _childViewConnection;
+ AnimationController _transitionAnimation;
+ CurvedAnimation _curvedTransitionAnimation;
+
+ /// Constructor.
+ AuthenticationOverlayModel() {
+ _transitionAnimation = new AnimationController(
+ value: 0.0,
+ duration: const Duration(seconds: 1),
+ vsync: this,
+ );
+ _curvedTransitionAnimation = new CurvedAnimation(
+ parent: _transitionAnimation,
+ curve: Curves.fastOutSlowIn,
+ reverseCurve: Curves.fastOutSlowIn,
+ );
+ }
+
+ /// If not null, returns the handle of the current requested overlay.
+ ChildViewConnection get childViewConnection => _childViewConnection;
+
+ /// The animation controlling the fading in and out of the authentication
+ /// overlay.
+ CurvedAnimation get animation => _curvedTransitionAnimation;
+
+ /// Starts showing an overlay over all other content.
+ void onStartOverlay(ViewHolderToken overlayViewHolderToken) {
+ _childViewConnection = ChildViewConnection(
+ overlayViewHolderToken,
+ onAvailable: (ChildViewConnection connection) {
+ log.fine(
+ 'AuthenticationOverlayModel: Child view connection available!',
+ );
+ _transitionAnimation.forward();
+ connection.requestFocus();
+ },
+ onUnavailable: (ChildViewConnection connection) {
+ log.fine(
+ 'AuthenticationOverlayModel: Child view connection unavailable!',
+ );
+ _transitionAnimation.reverse();
+ // TODO(apwilson): Should not need to remove the child view
+ // connection but it causes a scenic deadlock in the compositor if you
+ // don't.
+ _childViewConnection = null;
+ },
+ );
+ notifyListeners();
+ }
+
+ /// Stops showing a previously started overlay.
+ void onStopOverlay() {
+ _transitionAnimation.reverse();
+ // TODO(apwilson): Should not need to remove the child view
+ // connection but it causes a scenic deadlock in the compositor if you
+ // don't.
+ _childViewConnection = null;
+ notifyListeners();
+ }
+
+ @override
+ Ticker createTicker(TickerCallback onTick) => new Ticker(onTick);
+}
diff --git a/bin/userpicker_base_shell/lib/authentication_ui_context_impl.dart b/bin/userpicker_base_shell/lib/authentication_ui_context_impl.dart
new file mode 100644
index 0000000..9543fc5
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/authentication_ui_context_impl.dart
@@ -0,0 +1,46 @@
+// Copyright 2018 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 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_viewsv1token/fidl_async.dart';
+import 'package:flutter/widgets.dart';
+import 'package:zircon/zircon.dart';
+
+/// Called when an authentication overlay needs to be started.
+typedef OnStartOverlay = void Function(ViewHolderToken viewHolderToken);
+
+/// An [AuthenticationUiContext] which calls its callbacks to show an overlay.
+class AuthenticationUiContextImpl extends AuthenticationUiContext {
+ /// Called when an aunthentication overlay needs to be started.
+ final OnStartOverlay _onStartOverlay;
+
+ /// Called when an aunthentication overlay needs to be stopped.
+ final VoidCallback _onStopOverlay;
+
+ /// Builds an AuthenticationUiContext that takes |ViewHolderToken| callbacks
+ /// to start and stop an authentication display overlay.
+ AuthenticationUiContextImpl(
+ {OnStartOverlay onStartOverlay, VoidCallback onStopOverlay})
+ : _onStartOverlay = onStartOverlay,
+ _onStopOverlay = onStopOverlay;
+
+ @override
+ Future<void> startOverlay(InterfaceHandle<ViewOwner> viewOwner) =>
+ startOverlay2(new EventPair(viewOwner?.passChannel()?.passHandle()));
+
+ @override
+ // ignore: override_on_non_overriding_method
+ Future<void> startOverlay2(EventPair viewHolderToken) {
+ _onStartOverlay?.call(ViewHolderToken(value: viewHolderToken));
+ return null;
+ }
+
+ @override
+ Future<void> stopOverlay() {
+ _onStopOverlay?.call();
+ return null;
+ }
+}
diff --git a/bin/userpicker_base_shell/lib/circular_button.dart b/bin/userpicker_base_shell/lib/circular_button.dart
new file mode 100644
index 0000000..6beafab
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/circular_button.dart
@@ -0,0 +1,33 @@
+// Copyright 2017 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 'package:flutter/material.dart';
+import 'package:meta/meta.dart';
+
+/// A button that is circular
+class CircularButton extends StatelessWidget {
+ /// Callback that is fired when the button is tapped
+ final VoidCallback onTap;
+
+ /// The icon to show in the button
+ final IconData icon;
+
+ /// Constructor
+ const CircularButton({@required this.icon, this.onTap})
+ : assert(icon != null);
+
+ @override
+ Widget build(BuildContext context) => new Material(
+ type: MaterialType.circle,
+ elevation: 2.0,
+ color: Colors.grey[200],
+ child: new InkWell(
+ onTap: () => onTap?.call(),
+ child: new Container(
+ padding: const EdgeInsets.all(12.0),
+ child: new Icon(icon),
+ ),
+ ),
+ );
+}
diff --git a/bin/userpicker_base_shell/lib/clock.dart b/bin/userpicker_base_shell/lib/clock.dart
new file mode 100644
index 0000000..ed7e932
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/clock.dart
@@ -0,0 +1,41 @@
+// Copyright 2017 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 'dart:math';
+
+import 'package:flutter/material.dart';
+
+import 'time_stringer.dart';
+
+/// System Clock in the Base Shell
+class Clock extends StatelessWidget {
+ final TimeStringer _time = new TimeStringer();
+
+ @override
+ Widget build(BuildContext context) {
+ return new LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ return new AnimatedBuilder(
+ animation: _time,
+ builder: (BuildContext context, Widget child) {
+ return new Container(
+ child: new Text(
+ _time.timeOnly,
+ style: new TextStyle(
+ color: Colors.white,
+ fontSize: min(
+ constraints.maxWidth / 6.0,
+ constraints.maxHeight / 6.0,
+ ),
+ fontWeight: FontWeight.w200,
+ letterSpacing: 4.0,
+ ),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/bin/userpicker_base_shell/lib/main.dart b/bin/userpicker_base_shell/lib/main.dart
new file mode 100644
index 0000000..6161465
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/main.dart
@@ -0,0 +1,197 @@
+// Copyright 2017 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 'package:fidl_fuchsia_cobalt/fidl_async.dart' as cobalt;
+import 'package:fidl_fuchsia_mem/fidl_async.dart';
+import 'package:fidl_fuchsia_netstack/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:lib.base_shell/base_shell_widget.dart';
+import 'package:lib.base_shell/netstack_model.dart';
+import 'package:lib.widgets/application.dart';
+import 'package:lib.widgets/model.dart';
+import 'package:meta/meta.dart';
+import 'package:zircon/zircon.dart';
+
+import 'authentication_overlay.dart';
+import 'authentication_overlay_model.dart';
+import 'authentication_ui_context_impl.dart';
+import 'user_picker_base_shell_model.dart';
+import 'user_picker_base_shell_screen.dart';
+
+const double _kMousePointerElevation = 800.0;
+const double _kIndicatorElevation = _kMousePointerElevation - 1.0;
+
+const String _kCobaltConfigBinProtoPath = '/pkg/data/sysui_metrics_config.pb';
+
+/// The main base shell widget.
+BaseShellWidget<UserPickerBaseShellModel> _baseShellWidget;
+
+void main() {
+ setupLogger(name: 'userpicker_base_shell');
+ StartupContext startupContext = new StartupContext.fromStartupInfo();
+
+ // Connect to Cobalt
+ cobalt.LoggerProxy logger = new cobalt.LoggerProxy();
+
+ cobalt.LoggerFactoryProxy loggerFactory = new cobalt.LoggerFactoryProxy();
+ connectToEnvironmentService(loggerFactory);
+
+ SizedVmo configVmo = SizedVmo.fromFile(_kCobaltConfigBinProtoPath);
+ cobalt.ProjectProfile profile = cobalt.ProjectProfile(
+ config: Buffer(vmo: configVmo, size: configVmo.size),
+ releaseStage: cobalt.ReleaseStage.ga);
+ loggerFactory
+ .createLogger(profile, logger.ctrl.request())
+ .then((cobalt.Status s) {
+ if (s != cobalt.Status.ok) {
+ print('Failed to obtain Logger. Cobalt config is invalid.');
+ }
+ });
+ loggerFactory.ctrl.close();
+
+ NetstackProxy netstackProxy = new NetstackProxy();
+ connectToEnvironmentService(netstackProxy);
+
+ NetstackModel netstackModel = new NetstackModel(netstack: netstackProxy)
+ ..start();
+
+ _OverlayModel wifiInfoOverlayModel = new _OverlayModel();
+
+ final AuthenticationOverlayModel authModel = AuthenticationOverlayModel();
+
+ UserPickerBaseShellModel userPickerBaseShellModel =
+ new UserPickerBaseShellModel(
+ onBaseShellStopped: () {
+ netstackProxy.ctrl.close();
+ netstackModel.dispose();
+ },
+ onLogin: () {
+ wifiInfoOverlayModel.showing = false;
+ },
+ onWifiTapped: () {
+ wifiInfoOverlayModel.showing = !wifiInfoOverlayModel.showing;
+ },
+ logger: logger,
+ );
+
+ Widget mainWidget = new Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ new UserPickerBaseShellScreen(
+ launcher: startupContext.launcher,
+ ),
+ new ScopedModel<AuthenticationOverlayModel>(
+ model: authModel,
+ child: AuthenticationOverlay(),
+ ),
+ ],
+ );
+
+ Widget app = mainWidget;
+
+ List<OverlayEntry> overlays = <OverlayEntry>[
+ new OverlayEntry(
+ builder: (BuildContext context) => new MediaQuery(
+ data: const MediaQueryData(),
+ child: new FocusScope(
+ node: new FocusScopeNode(),
+ autofocus: true,
+ child: app,
+ ),
+ ),
+ ),
+ new OverlayEntry(
+ builder: (BuildContext context) => new ScopedModel<_OverlayModel>(
+ model: wifiInfoOverlayModel,
+ child: new _WifiInfo(
+ wifiWidget: new ApplicationWidget(
+ url:
+ 'fuchsia-pkg://fuchsia.com/wifi_settings#meta/wifi_settings.cmx',
+ launcher: startupContext.launcher,
+ ),
+ ),
+ ),
+ ),
+ ];
+
+ _baseShellWidget = new BaseShellWidget<UserPickerBaseShellModel>(
+ startupContext: startupContext,
+ baseShellModel: userPickerBaseShellModel,
+ authenticationUiContext: new AuthenticationUiContextImpl(
+ onStartOverlay: authModel.onStartOverlay,
+ onStopOverlay: authModel.onStopOverlay),
+ child: new LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) =>
+ (constraints.biggest == Size.zero)
+ ? const Offstage()
+ : new ScopedModel<NetstackModel>(
+ model: netstackModel,
+ child: new Overlay(initialEntries: overlays),
+ ),
+ ),
+ );
+
+ runApp(_baseShellWidget);
+
+ _baseShellWidget.advertise();
+}
+
+class _WifiInfo extends StatelessWidget {
+ final Widget wifiWidget;
+
+ const _WifiInfo({@required this.wifiWidget}) : assert(wifiWidget != null);
+
+ @override
+ Widget build(BuildContext context) =>
+ new ScopedModelDescendant<_OverlayModel>(
+ builder: (
+ BuildContext context,
+ Widget child,
+ _OverlayModel model,
+ ) =>
+ new Offstage(
+ offstage: !model.showing,
+ child: new Stack(
+ children: <Widget>[
+ new Listener(
+ behavior: HitTestBehavior.opaque,
+ onPointerDown: (PointerDownEvent event) {
+ model.showing = false;
+ },
+ ),
+ new Center(
+ child: new FractionallySizedBox(
+ widthFactor: 0.75,
+ heightFactor: 0.75,
+ child: new Container(
+ margin: const EdgeInsets.all(8.0),
+ child: new PhysicalModel(
+ color: Colors.grey[900],
+ elevation: _kIndicatorElevation,
+ borderRadius: new BorderRadius.circular(8.0),
+ child: wifiWidget,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+class _OverlayModel extends Model {
+ bool _showing = false;
+
+ set showing(bool showing) {
+ if (_showing != showing) {
+ _showing = showing;
+ notifyListeners();
+ }
+ }
+
+ bool get showing => _showing;
+}
diff --git a/bin/userpicker_base_shell/lib/time_stringer.dart b/bin/userpicker_base_shell/lib/time_stringer.dart
new file mode 100644
index 0000000..b8020b3
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/time_stringer.dart
@@ -0,0 +1,93 @@
+// Copyright 2016 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 'dart:async';
+
+import 'package:flutter/widgets.dart';
+import 'package:intl/intl.dart';
+
+final DateFormat _kTimeOnlyDateFormat = new DateFormat('h:mm', 'en_US');
+final DateFormat _kDateOnlyDateFormat = new DateFormat('EEEE MMM d', 'en_US');
+final DateFormat _kShortStringDateFormat = new DateFormat('h:mm', 'en_US');
+final DateFormat _kLongStringDateFormat = new DateFormat('EEEE h:mm', 'en_US');
+final DateFormat _kMeridiemOnlyFormat = new DateFormat('a', 'en_US');
+
+/// Creates time strings and notifies when they change.
+class TimeStringer extends Listenable {
+ final Set<VoidCallback> _listeners = <VoidCallback>{};
+ Timer _timer;
+ int _offsetMinutes = 0;
+
+ /// [listener] will be called whenever [shortString] or [longString] have
+ /// changed.
+ @override
+ void addListener(VoidCallback listener) {
+ _listeners.add(listener);
+ if (_listeners.length == 1) {
+ _scheduleTimer();
+ }
+ }
+
+ /// [listener] will no longer be called whenever [shortString] or [longString]
+ /// have changed.
+ @override
+ void removeListener(VoidCallback listener) {
+ _listeners.remove(listener);
+ if (_listeners.isEmpty) {
+ _timer?.cancel();
+ _timer = null;
+ }
+ }
+
+ /// Returns the time only (eg. '10:34').
+ String get timeOnly => _kTimeOnlyDateFormat
+ .format(
+ new DateTime.now(),
+ )
+ .toUpperCase();
+
+ /// Returns the date only (eg. 'MONDAY AUG 3').
+ String get dateOnly => _kDateOnlyDateFormat
+ .format(
+ new DateTime.now(),
+ )
+ .toUpperCase();
+
+ /// Returns a short version of the time (eg. '10:34').
+ String get shortString =>
+ _kShortStringDateFormat.format(new DateTime.now()).toLowerCase();
+
+ /// Returns a long version of the time including the day (eg. 'Monday 10:34').
+ String get longString =>
+ _kLongStringDateFormat.format(new DateTime.now()).toLowerCase();
+
+ /// Returns the meridiem (eg. 'AM')
+ String get meridiem =>
+ _kMeridiemOnlyFormat.format(new DateTime.now()).toUpperCase();
+
+ /// Returns the offset, in minutes.
+ int get offsetMinutes => _offsetMinutes;
+
+ set offsetMinutes(int offsetMinutes) {
+ if (_offsetMinutes != offsetMinutes) {
+ _offsetMinutes = offsetMinutes;
+ _notifyListeners();
+ }
+ }
+
+ void _scheduleTimer() {
+ _timer?.cancel();
+ _timer =
+ new Timer(new Duration(seconds: 61 - new DateTime.now().second), () {
+ _notifyListeners();
+ _scheduleTimer();
+ });
+ }
+
+ void _notifyListeners() {
+ for (VoidCallback listener in _listeners.toList()) {
+ listener();
+ }
+ }
+}
diff --git a/bin/userpicker_base_shell/lib/user_list.dart b/bin/userpicker_base_shell/lib/user_list.dart
new file mode 100644
index 0000000..a04b479
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/user_list.dart
@@ -0,0 +1,388 @@
+// Copyright 2017 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 'package:fidl_fuchsia_modular_auth/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:lib.widgets/model.dart';
+import 'package:lib.widgets/widgets.dart';
+
+import 'user_picker_base_shell_model.dart';
+
+const double _kUserAvatarSizeLarge = 56.0;
+const double _kUserAvatarSizeSmall = 48.0;
+const double _kButtonWidthLarge = 128.0;
+const double _kButtonWidthSmall = 116.0;
+const double _kButtonFontSizeLarge = 16.0;
+const double _kButtonFontSizeSmall = 14.0;
+
+const TextStyle _kTextStyle = const TextStyle(
+ color: Colors.white,
+ fontSize: 10.0,
+ letterSpacing: 1.0,
+ fontWeight: FontWeight.w300,
+);
+
+final BorderRadius _kButtonBorderRadiusPhone =
+ new BorderRadius.circular(_kUserAvatarSizeSmall / 2.0);
+final BorderRadius _kButtonBorderRadiusLarge =
+ new BorderRadius.circular(_kUserAvatarSizeLarge / 2.0);
+
+/// Shows the list of users and allows the user to add new users
+class UserList extends StatelessWidget {
+ /// True if login should be disabled.
+ final bool loginDisabled;
+
+ /// Constructor.
+ const UserList({this.loginDisabled = false});
+
+ Widget _buildUserCircle({
+ Account account,
+ VoidCallback onTap,
+ bool isSmall,
+ }) {
+ double size = isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge;
+ return new GestureDetector(
+ onTap: () => onTap?.call(),
+ child: new Container(
+ height: size,
+ width: size,
+ child: new Alphatar.fromNameAndUrl(
+ name: account.displayName,
+ avatarUrl: _getImageUrl(account),
+ size: size,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildIconButton({
+ VoidCallback onTap,
+ bool isSmall,
+ IconData icon,
+ }) {
+ double size = isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge;
+ return _buildUserActionButton(
+ onTap: () => onTap?.call(),
+ width: size,
+ isSmall: isSmall,
+ child: new Center(
+ child: new Icon(
+ icon,
+ color: Colors.white,
+ size: size / 2.0,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildUserActionButton({
+ Widget child,
+ VoidCallback onTap,
+ bool isSmall,
+ double width,
+ bool isDisabled = false,
+ }) {
+ return new GestureDetector(
+ onTap: isDisabled ? null : () => onTap?.call(),
+ child: new Container(
+ height: isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge,
+ width: width ?? (isSmall ? _kButtonWidthSmall : _kButtonWidthLarge),
+ alignment: FractionalOffset.center,
+ margin: const EdgeInsets.only(left: 16.0),
+ decoration: new BoxDecoration(
+ borderRadius:
+ isSmall ? _kButtonBorderRadiusPhone : _kButtonBorderRadiusLarge,
+ border: new Border.all(
+ color: isDisabled ? Colors.grey : Colors.white,
+ width: 1.0,
+ ),
+ ),
+ child: child,
+ ),
+ );
+ }
+
+ Widget _buildExpandedUserActions({
+ UserPickerBaseShellModel model,
+ bool isSmall,
+ }) {
+ double fontSize = isSmall ? _kButtonFontSizeSmall : _kButtonFontSizeLarge;
+
+ if (loginDisabled) {
+ return new Row(
+ children: <Widget>[
+ _buildUserActionButton(
+ child: new Text(
+ 'RESET',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: model.resetTapped,
+ isSmall: isSmall,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'WIFI',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: model.wifiTapped,
+ isSmall: isSmall,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'LOGIN DISABLED => No SessionShell configured',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: () {},
+ isSmall: isSmall,
+ isDisabled: true,
+ ),
+ ],
+ );
+ }
+ return new Row(
+ children: <Widget>[
+ _buildIconButton(
+ onTap: () => model.hideUserActions(),
+ isSmall: isSmall,
+ icon: Icons.close,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'RESET',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: model.resetTapped,
+ isSmall: isSmall,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'WIFI',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: model.wifiTapped,
+ isSmall: isSmall,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'LOGIN',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: () {
+ model
+ ..createAndLoginUser()
+ ..hideUserActions();
+ },
+ isSmall: isSmall,
+ ),
+ _buildUserActionButton(
+ child: new Text(
+ 'GUEST',
+ style: new TextStyle(
+ fontSize: fontSize,
+ color: Colors.white,
+ ),
+ ),
+ onTap: () {
+ model
+ ..login(null)
+ ..hideUserActions();
+ },
+ isSmall: isSmall,
+ ),
+ ],
+ );
+ }
+
+ String _getImageUrl(Account account) {
+ if (account.imageUrl == null) {
+ return null;
+ }
+ Uri uri = Uri.parse(account.imageUrl);
+ if (uri.queryParameters['sz'] != null) {
+ Map<String, dynamic> queryParameters = new Map<String, dynamic>.from(
+ uri.queryParameters,
+ );
+ queryParameters['sz'] = '160';
+ uri = uri.replace(queryParameters: queryParameters);
+ }
+ return uri.toString();
+ }
+
+ Widget _buildUserEntry({
+ Account account,
+ VoidCallback onTap,
+ bool removable = true,
+ bool isSmall,
+ UserPickerBaseShellModel model,
+ }) {
+ Widget userCard = _buildUserCircle(
+ account: account,
+ onTap: onTap,
+ isSmall: isSmall,
+ );
+
+ if (!removable) {
+ return userCard;
+ }
+
+ Widget userImage = new LongPressDraggable<Account>(
+ child: userCard,
+ feedback: userCard,
+ data: account,
+ childWhenDragging: new Opacity(opacity: 0.0, child: userCard),
+ feedbackOffset: Offset.zero,
+ dragAnchor: DragAnchor.child,
+ maxSimultaneousDrags: 1,
+ onDragStarted: () => model.addDraggedUser(account),
+ onDraggableCanceled: (_, __) => model.removeDraggedUser(account),
+ );
+
+ if (model.showingUserActions) {
+ return new Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: new Column(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: <Widget>[
+ new Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: new Text(
+ account.displayName,
+ textAlign: TextAlign.center,
+ maxLines: 1,
+ style: _kTextStyle,
+ ),
+ ),
+ userImage,
+ ],
+ ),
+ );
+ } else {
+ return new Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: userImage,
+ );
+ }
+ }
+
+ Widget _buildUserList(UserPickerBaseShellModel model) {
+ return new LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ List<Widget> children = <Widget>[];
+
+ bool isSmall =
+ constraints.maxWidth < 600.0 || constraints.maxHeight < 600.0;
+
+ if (model.showingUserActions) {
+ children.add(
+ new Align(
+ alignment: FractionalOffset.bottomCenter,
+ child: _buildExpandedUserActions(
+ model: model,
+ isSmall: isSmall,
+ ),
+ ),
+ );
+ } else {
+ children.add(
+ new Align(
+ alignment: FractionalOffset.bottomCenter,
+ child: _buildIconButton(
+ onTap: model.showUserActions,
+ isSmall: isSmall,
+ icon: Icons.add,
+ ),
+ ),
+ );
+ }
+
+ children.addAll(
+ model.accounts.map(
+ (Account account) => new Align(
+ alignment: FractionalOffset.bottomCenter,
+ child: _buildUserEntry(
+ account: account,
+ onTap: () {
+ model
+ ..login(account.id)
+ ..hideUserActions();
+ },
+ isSmall: isSmall,
+ model: model,
+ ),
+ ),
+ ),
+ );
+
+ return new Container(
+ height: (isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge) +
+ 24.0 +
+ (model.showingUserActions ? 24.0 : 0.0),
+ child: new AnimatedOpacity(
+ duration: const Duration(milliseconds: 250),
+ opacity: model.showingRemoveUserTarget ? 0.0 : 1.0,
+ child: new ListView(
+ padding: const EdgeInsets.only(
+ bottom: 24.0,
+ right: 24.0,
+ ),
+ scrollDirection: Axis.horizontal,
+ reverse: true,
+ physics: const BouncingScrollPhysics(),
+ shrinkWrap: true,
+ children: children,
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) =>
+ new ScopedModelDescendant<UserPickerBaseShellModel>(builder: (
+ BuildContext context,
+ Widget child,
+ UserPickerBaseShellModel model,
+ ) {
+ if (model.showingLoadingSpinner) {
+ return new Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ new Center(
+ child: new Container(
+ width: 64.0,
+ height: 64.0,
+ child: const FuchsiaSpinner(),
+ ),
+ ),
+ ],
+ );
+ } else {
+ return new Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ _buildUserList(model),
+ ],
+ );
+ }
+ });
+}
diff --git a/bin/userpicker_base_shell/lib/user_picker_base_shell_model.dart b/bin/userpicker_base_shell/lib/user_picker_base_shell_model.dart
new file mode 100644
index 0000000..ceef118
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/user_picker_base_shell_model.dart
@@ -0,0 +1,182 @@
+// Copyright 2017 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 'dart:async';
+import 'dart:io';
+
+import 'package:fidl_fuchsia_cobalt/fidl_async.dart' as cobalt;
+import 'package:fidl_fuchsia_modular_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:lib.base_shell/base_model.dart';
+import 'package:lib.widgets/model.dart';
+
+export 'package:lib.widgets/model.dart'
+ show ScopedModel, ScopedModelDescendant, ModelFinder;
+
+/// Function signature for GetPresentationMode callback
+typedef GetPresentationModeCallback = void Function(PresentationMode mode);
+
+const Duration _kShowLoadingSpinnerDelay = const Duration(milliseconds: 500);
+
+/// Model that provides common state
+class UserPickerBaseShellModel extends CommonBaseShellModel
+ with TickerProviderModelMixin
+ implements
+ ServiceProvider,
+ KeyboardCaptureListenerHack,
+ PointerCaptureListenerHack {
+ /// Called when the base shell stops.
+ final VoidCallback onBaseShellStopped;
+
+ /// Called when wifi is tapped.
+ final VoidCallback onWifiTapped;
+
+ /// Called when a user is logging in.
+ final VoidCallback onLogin;
+
+ bool _showingUserActions = false;
+ bool _addingUser = false;
+ bool _loadingChildView = false;
+ final Set<Account> _draggedUsers = <Account>{};
+
+ /// Constructor
+ UserPickerBaseShellModel({
+ this.onBaseShellStopped,
+ this.onWifiTapped,
+ this.onLogin,
+ cobalt.Logger logger,
+ }) : super(logger);
+
+ @override
+ void onStop() {
+ onBaseShellStopped?.call();
+ super.dispose();
+ super.onStop();
+ }
+
+ /// Refreshes the list of users.
+ @override
+ Future<void> refreshUsers() async {
+ _updateShowLoadingSpinner();
+ notifyListeners();
+ await super.refreshUsers();
+ _updateShowLoadingSpinner();
+ notifyListeners();
+ }
+
+ /// Call when wifi is tapped.
+ void wifiTapped() {
+ onWifiTapped?.call();
+ }
+
+ /// Call when reset is tapped.
+ void resetTapped() {
+ File dm = new File('/dev/misc/dmctl');
+ print('dmctl exists? ${dm.existsSync()}');
+ if (dm.existsSync()) {
+ dm.writeAsStringSync('reboot', flush: true);
+ }
+ }
+
+ /// Create a new user and login with that user
+ @override
+ Future createAndLoginUser() async {
+ _addingUser = true;
+ _updateShowLoadingSpinner();
+
+ await super.createAndLoginUser();
+
+ _addingUser = false;
+ _updateShowLoadingSpinner();
+ notifyListeners();
+ }
+
+ /// Login with given user
+ @override
+ Future<void> login(String accountId) async {
+ _loadingChildView = true;
+ _updateShowLoadingSpinner();
+ notifyListeners();
+
+ await super.login(accountId);
+
+ _loadingChildView = false;
+ _updateShowLoadingSpinner();
+ notifyListeners();
+ }
+
+ /// Show advanced user actions such as:
+ /// * Guest login
+ /// * Create new account
+ void showUserActions() {
+ _showingUserActions = true;
+ notifyListeners();
+ }
+
+ /// Hide advanced user actions such as:
+ void hideUserActions() {
+ _showingUserActions = false;
+ notifyListeners();
+ }
+
+ /// Add a user to the list of dragged users
+ void addDraggedUser(Account account) {
+ _draggedUsers.add(account);
+ notifyListeners();
+ }
+
+ /// Remove a user from the list of dragged users
+ void removeDraggedUser(Account account) {
+ _draggedUsers.remove(account);
+ notifyListeners();
+ }
+
+ /// Show the loading spinner if true
+ bool get showingLoadingSpinner => _showingLoadingSpinner;
+
+ /// Show the system clock if true
+ bool get showingClock =>
+ !showingLoadingSpinner &&
+ _draggedUsers.isEmpty &&
+ childViewConnection == null;
+
+ /// If true, show advanced user actions
+ bool get showingUserActions => _showingUserActions;
+
+ /// If true, show the remove user target
+ bool get showingRemoveUserTarget => _draggedUsers.isNotEmpty;
+
+ /// Returns true the add user dialog is showing
+ bool get addingUser => _addingUser;
+
+ /// Returns true if we are "loading" the child view
+ bool get loadingChildView => _loadingChildView;
+
+ bool _showingLoadingSpinner = true;
+ Timer _showLoadingSpinnerTimer;
+
+ void _updateShowLoadingSpinner() {
+ if (accounts == null || _addingUser || _loadingChildView) {
+ if (_showingLoadingSpinner == null) {
+ _showLoadingSpinnerTimer = new Timer(
+ _kShowLoadingSpinnerDelay,
+ () {
+ _showingLoadingSpinner = true;
+ _showLoadingSpinnerTimer = null;
+ notifyListeners();
+ },
+ );
+ }
+ } else {
+ _showLoadingSpinnerTimer?.cancel();
+ _showLoadingSpinnerTimer = null;
+ _showingLoadingSpinner = false;
+ notifyListeners();
+ }
+ }
+}
diff --git a/bin/userpicker_base_shell/lib/user_picker_base_shell_screen.dart b/bin/userpicker_base_shell/lib/user_picker_base_shell_screen.dart
new file mode 100644
index 0000000..159f8fa
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/user_picker_base_shell_screen.dart
@@ -0,0 +1,60 @@
+// Copyright 2017 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 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:meta/meta.dart';
+
+import 'clock.dart';
+import 'user_picker_base_shell_model.dart';
+import 'user_picker_screen.dart';
+
+/// The root widget which displays all the other windows of this app.
+class UserPickerBaseShellScreen extends StatelessWidget {
+ /// Launcher to launch the kernel panic module if needed.
+ final Launcher launcher;
+
+ /// Constructor.
+ const UserPickerBaseShellScreen({
+ @required this.launcher,
+ Key key,
+ }) : super(key: key);
+ @override
+ Widget build(BuildContext context) {
+ return new ScopedModelDescendant<UserPickerBaseShellModel>(
+ builder: (
+ BuildContext context,
+ Widget child,
+ UserPickerBaseShellModel model,
+ ) {
+ List<Widget> stackChildren = <Widget>[];
+
+ if (model.childViewConnection == null || model.loadingChildView) {
+ stackChildren.addAll(<Widget>[
+ new UserPickerScreen(),
+ new Align(
+ alignment: FractionalOffset.center,
+ child: new Offstage(
+ offstage: !model.showingClock,
+ child: new Clock(),
+ ),
+ ),
+ ]);
+ }
+
+ if (model.childViewConnection != null) {
+ stackChildren.add(new Offstage(
+ child: new Container(
+ color: Colors.black,
+ child: new ChildView(connection: model.childViewConnection),
+ ),
+ offstage: model.loadingChildView,
+ ));
+ }
+
+ return new Stack(fit: StackFit.expand, children: stackChildren);
+ },
+ );
+ }
+}
diff --git a/bin/userpicker_base_shell/lib/user_picker_screen.dart b/bin/userpicker_base_shell/lib/user_picker_screen.dart
new file mode 100644
index 0000000..0a73bc0
--- /dev/null
+++ b/bin/userpicker_base_shell/lib/user_picker_screen.dart
@@ -0,0 +1,192 @@
+// Copyright 2017 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 'dart:ui' show lerpDouble;
+
+import 'package:fidl_fuchsia_modular_auth/fidl_async.dart';
+import 'package:flutter/material.dart';
+
+import 'user_list.dart';
+import 'user_picker_base_shell_model.dart';
+
+const double _kRemovalTargetSize = 112.0;
+
+/// Displays a [UserList] a shutdown button, a new user button, the
+/// fuchsia logo, and a background image.
+class UserPickerScreen extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return new ScopedModelDescendant<UserPickerBaseShellModel>(
+ builder: (
+ BuildContext context,
+ Widget child,
+ UserPickerBaseShellModel model,
+ ) {
+ return new Material(
+ color: Colors.grey[900],
+ child: new Stack(
+ fit: StackFit.passthrough,
+ children: <Widget>[
+ /// Add user picker for selecting users and adding new users
+ new Align(
+ alignment: FractionalOffset.bottomRight,
+ child: new RepaintBoundary(
+ child: new UserList(
+ loginDisabled: false,
+ ),
+ ),
+ ),
+
+ // Add user removal target
+ new Align(
+ alignment: FractionalOffset.center,
+ child: new RepaintBoundary(
+ child: new Container(
+ child: new DragTarget<Account>(
+ onWillAccept: (Account data) => true,
+ onAccept: model.removeUser,
+ builder: (
+ _,
+ List<Account> candidateData,
+ __,
+ ) =>
+ new _UserRemovalTarget(
+ show: model.showingRemoveUserTarget,
+ grow: candidateData.isNotEmpty,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
+
+/// Displays a removal target for removing users
+class _UserRemovalTarget extends StatefulWidget {
+ /// Grows the target by some percentage.
+ final bool grow;
+
+ /// Shows the target.
+ final bool show;
+
+ /// Constructor.
+ const _UserRemovalTarget({this.show, this.grow});
+
+ @override
+ _UserRemovalTargetState createState() => new _UserRemovalTargetState();
+}
+
+class _UserRemovalTargetState extends State<_UserRemovalTarget>
+ with TickerProviderStateMixin {
+ AnimationController _masterAnimationController;
+ AnimationController _initialScaleController;
+ CurvedAnimation _initialScaleCurvedAnimation;
+ AnimationController _scaleController;
+ CurvedAnimation _scaleCurvedAnimation;
+
+ @override
+ void initState() {
+ super.initState();
+ _masterAnimationController = new AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 500),
+ );
+ _initialScaleController = new AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 250),
+ );
+ _initialScaleCurvedAnimation = new CurvedAnimation(
+ parent: _initialScaleController,
+ curve: Curves.fastOutSlowIn,
+ reverseCurve: Curves.fastOutSlowIn,
+ );
+ _scaleController = new AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 250),
+ );
+ _scaleCurvedAnimation = new CurvedAnimation(
+ parent: _scaleController,
+ curve: Curves.fastOutSlowIn,
+ reverseCurve: Curves.fastOutSlowIn,
+ );
+ _initialScaleController.addStatusListener((AnimationStatus status) {
+ if (!widget.show && _initialScaleController.isDismissed) {
+ _masterAnimationController.stop();
+ }
+ });
+
+ if (widget.show) {
+ _masterAnimationController.repeat();
+ _initialScaleController.forward();
+ if (widget.grow) {
+ _scaleController.forward();
+ }
+ }
+ }
+
+ @override
+ void didUpdateWidget(_) {
+ super.didUpdateWidget(_);
+ if (widget.grow) {
+ _scaleController.forward();
+ } else {
+ _scaleController.reverse();
+ }
+ if (widget.show) {
+ _masterAnimationController.repeat();
+ _initialScaleController.forward();
+ } else {
+ _initialScaleController.value = 0.0;
+ }
+ }
+
+ @override
+ void dispose() {
+ _scaleController.dispose();
+ _initialScaleController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => new Container(
+ child: new AnimatedBuilder(
+ animation: _masterAnimationController,
+ builder: (BuildContext context, Widget child) => new Transform(
+ alignment: FractionalOffset.center,
+ transform: new Matrix4.identity().scaled(
+ lerpDouble(1.0, 0.7, _scaleCurvedAnimation.value) *
+ _initialScaleCurvedAnimation.value,
+ lerpDouble(1.0, 0.7, _scaleCurvedAnimation.value) *
+ _initialScaleCurvedAnimation.value,
+ ),
+ child: new Container(
+ width: _kRemovalTargetSize,
+ height: _kRemovalTargetSize,
+ decoration: new BoxDecoration(
+ borderRadius:
+ new BorderRadius.circular(_kRemovalTargetSize / 2.0),
+ border: new Border.all(color: Colors.white.withAlpha(200)),
+ color: Colors.white.withAlpha(
+ lerpDouble(0, 100.0, _scaleCurvedAnimation.value)
+ .toInt()),
+ ),
+ child: Center(
+ child: const Text(
+ 'REMOVE',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 16.0,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/bin/userpicker_base_shell/meta/userpicker_base_shell.cmx b/bin/userpicker_base_shell/meta/userpicker_base_shell.cmx
new file mode 100644
index 0000000..4c1b420
--- /dev/null
+++ b/bin/userpicker_base_shell/meta/userpicker_base_shell.cmx
@@ -0,0 +1,23 @@
+{
+ "program": {
+ "data": "data/userpicker_base_shell"
+ },
+ "sandbox": {
+ "services": [
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.fonts.Provider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.modular.Clipboard",
+ "fuchsia.modular.ContextWriter",
+ "fuchsia.net.Connectivity",
+ "fuchsia.netstack.Netstack",
+ "fuchsia.sys.Environment",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.policy.Presenter",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.viewsv1.ViewManager"
+ ],
+ "dev": [ "misc" ],
+ "system": [ "data/sysui" ]
+ }
+}
diff --git a/bin/userpicker_base_shell/pubspec.yaml b/bin/userpicker_base_shell/pubspec.yaml
new file mode 100644
index 0000000..76bca92
--- /dev/null
+++ b/bin/userpicker_base_shell/pubspec.yaml
@@ -0,0 +1,5 @@
+# Copyright 2017 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.
+
+name: userpicker_base_shell
diff --git a/lib/base_shell/BUILD.gn b/lib/base_shell/BUILD.gn
new file mode 100644
index 0000000..6bfb266
--- /dev/null
+++ b/lib/base_shell/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2018 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/dart/dart_library.gni")
+
+dart_library("lib.base_shell") {
+ package_name = "lib.base_shell"
+ sources_required = false
+
+ sources = [
+ "base_model.dart",
+ "netstack_model.dart",
+ "user_manager.dart",
+ # Including these sources triggers analysis errors.
+ # "base_shell_model.dart",
+ # "base_shell_widget.dart",
+ ]
+
+ deps = [
+ "//sdk/fidl/fuchsia.modular",
+ "//sdk/fidl/fuchsia.modular.auth",
+ "//sdk/fidl/fuchsia.netstack",
+ "//sdk/fidl/fuchsia.timezone",
+ "//sdk/fidl/fuchsia.ui.input",
+ "//sdk/fidl/fuchsia.ui.views",
+ "//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//topaz/public/dart/fidl",
+ "//topaz/public/dart/fuchsia_scenic_flutter",
+ "//topaz/public/dart/widgets:lib.widgets",
+ "//zircon/public/fidl/fuchsia-net:fuchsia-net",
+ ]
+}
diff --git a/lib/base_shell/analysis_options.yaml b/lib/base_shell/analysis_options.yaml
new file mode 100644
index 0000000..49a21d7
--- /dev/null
+++ b/lib/base_shell/analysis_options.yaml
@@ -0,0 +1,6 @@
+# Copyright 2018 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.
+
+include: ../analysis_options.yaml
+
diff --git a/lib/base_shell/lib/base_model.dart b/lib/base_shell/lib/base_model.dart
new file mode 100644
index 0000000..0f6ce90
--- /dev/null
+++ b/lib/base_shell/lib/base_model.dart
@@ -0,0 +1,394 @@
+// Copyright 2017 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 'dart:async';
+import 'dart:developer' show Timeline;
+
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_cobalt/fidl_async.dart' as cobalt;
+import 'package:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:fidl_fuchsia_modular_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_netstack/fidl_async.dart';
+import 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_gfx/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_input/fidl_async.dart' as input;
+import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
+ show ChildViewConnection;
+import 'package:fuchsia_services/services.dart' as app;
+import 'package:meta/meta.dart';
+import 'package:zircon/zircon.dart' show Channel, EventPair;
+
+import 'base_shell_model.dart';
+import 'netstack_model.dart';
+import 'user_manager.dart';
+
+export 'package:lib.widgets/model.dart'
+ show ScopedModel, ScopedModelDescendant, ModelFinder;
+
+/// Function signature for GetPresentationMode callback
+typedef GetPresentationModeCallback = void Function(PresentationMode mode);
+
+const Duration _kCobaltTimerTimeout = const Duration(seconds: 20);
+const int _kSessionShellLoginTimeMetricId = 14;
+
+// This class is extends the Presentation protocol and implements and PresentationModeListener.
+// It delegates the methods to the Presentation received by the CommonBaseShellModel that owns it.
+class CommonBaseShellPresentationImpl extends Presentation
+ implements PresentationModeListener {
+ final CommonBaseShellModel _model;
+
+ CommonBaseShellPresentationImpl(this._model);
+
+ /// |Presentation|.
+ @override
+ // ignore: avoid_positional_boolean_parameters
+ Future<void> enableClipping(bool enabled) async {
+ await _model.presentation.enableClipping(enabled);
+ }
+
+ @override
+ Future<void> useOrthographicView() async {
+ await _model.presentation.useOrthographicView();
+ }
+
+ @override
+ Future<void> usePerspectiveView() async {
+ await _model.presentation.usePerspectiveView();
+ }
+
+ @override
+ Future<void> setRendererParams(List<RendererParam> params) async {
+ await _model.presentation.setRendererParams(params);
+ }
+
+ @override
+ Future<void> setDisplayUsage(DisplayUsage usage) async {
+ await _model.presentation.setDisplayUsage(usage);
+ }
+
+ @override
+ // ignore: avoid_positional_boolean_parameters
+ Future<void> setDisplayRotation(
+ double displayRotationDegrees, bool animate) async {
+ await _model.presentation
+ .setDisplayRotation(displayRotationDegrees, animate);
+ }
+
+ @override
+ Future<void> setDisplaySizeInMm(num widthInMm, num heightInMm) async {
+ await _model.presentation.setDisplaySizeInMm(widthInMm, heightInMm);
+ }
+
+ @override
+ Future<void> captureKeyboardEventHack(input.KeyboardEvent eventToCapture,
+ InterfaceHandle<KeyboardCaptureListenerHack> listener) async {
+ await _model.presentation
+ .captureKeyboardEventHack(eventToCapture, listener);
+ }
+
+ @override
+ Future<void> capturePointerEventsHack(
+ InterfaceHandle<PointerCaptureListenerHack> listener) async {
+ await _model.presentation.capturePointerEventsHack(listener);
+ }
+
+ @override
+ Future<PresentationMode> getPresentationMode() async {
+ return await _model.presentation.getPresentationMode();
+ }
+
+ @override
+ Future<void> setPresentationModeListener(
+ InterfaceHandle<PresentationModeListener> listener) async {
+ await _model.presentation.setPresentationModeListener(listener);
+ }
+
+ /// |PresentationModeListener|.
+ @override
+ Future<void> onModeChanged() async {
+ PresentationMode mode = await getPresentationMode();
+ log.info('Presentation mode changed to: $mode');
+ switch (mode) {
+ case PresentationMode.tent:
+ await setDisplayRotation(180.0, true);
+ break;
+ case PresentationMode.tablet:
+ // TODO(sanjayc): Figure out up/down orientation.
+ await setDisplayRotation(90.0, true);
+ break;
+ case PresentationMode.laptop:
+ default:
+ await setDisplayRotation(0.0, true);
+ break;
+ }
+ }
+}
+
+/// Provides common features needed by all base shells.
+///
+/// This includes user management, presentation handling,
+/// and keyboard shortcuts.
+class CommonBaseShellModel extends BaseShellModel
+ implements
+ ServiceProvider,
+ KeyboardCaptureListenerHack,
+ PointerCaptureListenerHack {
+ /// Handles login, logout, and adding/removing users.
+ ///
+ /// Shouldn't be used before onReady.
+ BaseShellUserManager _userManager;
+
+ NetstackModel _netstackModel;
+
+ /// Logs metrics to cobalt.
+ final cobalt.Logger logger;
+
+ /// A list of accounts that are already logged in on the device.
+ ///
+ /// Only updated after [refreshUsers] is called.
+ List<Account> _accounts;
+
+ /// Childview connection that contains the session shell.
+ ChildViewConnection _childViewConnection;
+
+ final List<KeyboardCaptureListenerHackBinding> _keyBindings = [];
+
+ final PresentationModeListenerBinding _presentationModeListenerBinding =
+ PresentationModeListenerBinding();
+ final PointerCaptureListenerHackBinding _pointerCaptureListenerBinding =
+ PointerCaptureListenerHackBinding();
+
+ // Because this base shell only supports a single user logged in at a time,
+ // we don't need to maintain separate ServiceProvider for each logged-in user.
+ final ServiceProviderBinding _serviceProviderBinding =
+ ServiceProviderBinding();
+ final List<PresentationBinding> _presentationBindings =
+ <PresentationBinding>[];
+
+ CommonBaseShellPresentationImpl _presentationImpl;
+
+ /// Constructor
+ CommonBaseShellModel(this.logger) : super() {
+ _presentationImpl = CommonBaseShellPresentationImpl(this);
+ }
+
+ List<Account> get accounts => _accounts;
+
+ /// Returns the authenticated child view connection
+ ChildViewConnection get childViewConnection => _childViewConnection;
+ ChildViewConnection get childViewConnectionNew => _childViewConnection;
+
+ set shouldCreateNewChildView(bool should) {}
+
+ // |ServiceProvider|.
+ @override
+ Future<void> connectToService(String serviceName, Channel channel) {
+ // TODO(SCN-595) mozart.Presentation is being renamed to ui.Presentation.
+ if (serviceName == 'ui.Presentation') {
+ _presentationBindings.add(PresentationBinding()
+ ..bind(_presentationImpl, InterfaceRequest<Presentation>(channel)));
+ } else {
+ log.warning(
+ 'UserPickerBaseShell: received request for unknown service: $serviceName !');
+ channel.close();
+ }
+
+ return null;
+ }
+
+ /// Create a new user and login with that user
+ Future createAndLoginUser() async {
+ try {
+ final userId = await _userManager.addUser();
+ await login(userId);
+ } on UserLoginException catch (ex) {
+ log.severe(ex);
+ } finally {
+ notifyListeners();
+ }
+ }
+
+ /// Whether or not the device has an internet connection.
+ ///
+ /// Currently, having an IP is equivalent to having internet, although
+ /// this is not completely reliable. This will be always false until
+ /// onReady is called.
+ bool get hasInternetConnection =>
+ _netstackModel?.networkReachable?.value ?? false;
+
+ Future<void> waitForInternetConnection() async {
+ if (hasInternetConnection) {
+ return null;
+ }
+
+ final completer = Completer<void>();
+
+ void listener() {
+ if (hasInternetConnection) {
+ _netstackModel.removeListener(listener);
+ completer.complete();
+ }
+ }
+
+ _netstackModel.addListener(listener);
+
+ return completer.future;
+ }
+
+ /// Login with given user
+ Future<void> login(String accountId) async {
+ if (_serviceProviderBinding.isBound) {
+ log.warning(
+ 'Ignoring unsupported attempt to log in'
+ ' while already logged in!',
+ );
+ return;
+ }
+
+ Timeline.instantSync('logging in', arguments: {'accountId': '$accountId'});
+ await logger
+ .startTimer(
+ _kSessionShellLoginTimeMetricId,
+ 0,
+ '',
+ 'session_shell_login_timer_id',
+ DateTime.now().millisecondsSinceEpoch,
+ _kCobaltTimerTimeout.inSeconds)
+ .then((status) {
+ if (status != cobalt.Status.ok) {
+ log.warning(
+ 'Failed to start timer metric '
+ '$_kSessionShellLoginTimeMetricId: $status. ',
+ );
+ }
+ });
+
+ final InterfacePair<ServiceProvider> serviceProvider =
+ InterfacePair<ServiceProvider>();
+
+ _serviceProviderBinding.bind(this, serviceProvider.passRequest());
+
+ final viewOwnerHandle =
+ _userManager.login(accountId, serviceProvider.passHandle());
+
+ _childViewConnection = ChildViewConnection(
+ ViewHolderToken(
+ value: EventPair(viewOwnerHandle.passChannel().passHandle())),
+ onAvailable: (ChildViewConnection connection) {
+ log.info('BaseShell: Child view connection available!');
+ connection.requestFocus();
+ notifyListeners();
+ },
+ onUnavailable: (ChildViewConnection connection) {
+ log.info('BaseShell: Child view connection now unavailable!');
+ onLogout();
+ notifyListeners();
+ },
+ );
+
+ notifyListeners();
+ }
+
+ /// Called when the the session shell logs out.
+ @mustCallSuper
+ Future<void> onLogout() async {
+ _childViewConnection = null;
+ _serviceProviderBinding.close();
+ for (PresentationBinding presentationBinding in _presentationBindings) {
+ presentationBinding.close();
+ }
+ await refreshUsers();
+ notifyListeners();
+ }
+
+ /// |KeyboardCaptureListener|.
+ @override
+ Future<void> onEvent(input.KeyboardEvent ev) async {}
+
+ /// |PointerCaptureListener|.
+ @override
+ Future<void> onPointerEvent(input.PointerEvent event) async {}
+
+ // |BaseShellModel|.
+ // TODO: revert to default state when client logs out.
+ @mustCallSuper
+ @override
+ Future<void> onReady(
+ UserProvider userProvider,
+ BaseShellContext baseShellContext,
+ Presentation presentation,
+ ) async {
+ super.onReady(userProvider, baseShellContext, presentation);
+
+ final netstackProxy = NetstackProxy();
+ app.connectToEnvironmentService(netstackProxy);
+ _netstackModel = NetstackModel(netstack: netstackProxy)..start();
+
+ await presentation
+ .capturePointerEventsHack(_pointerCaptureListenerBinding.wrap(this));
+ await presentation.setPresentationModeListener(
+ _presentationModeListenerBinding.wrap(_presentationImpl));
+
+ _userManager = BaseShellUserManager(userProvider);
+
+ _userManager.onLogout.listen((_) async {
+ await logger
+ .endTimer(
+ 'session_shell_log_out_timer_id',
+ DateTime.now().millisecondsSinceEpoch,
+ _kCobaltTimerTimeout.inSeconds)
+ .then((status) {
+ if (status != cobalt.Status.ok) {
+ log.warning(
+ 'Failed to end timer metric '
+ 'session_shell_log_out_timer_id: $status. ',
+ );
+ }
+ });
+
+ log.info('UserPickerBaseShell: User logged out!');
+ await onLogout();
+ });
+
+ await refreshUsers();
+ }
+
+ // |BaseShellModel|
+ // TODO: revert to default state when client logs out.
+ @override
+ void onStop() {
+ for (final binding in _keyBindings) {
+ binding.close();
+ }
+ _presentationModeListenerBinding.close();
+ _netstackModel.dispose();
+ super.onStop();
+ }
+
+ // TODO: revert to default state when client logs out.
+ /// Refreshes the list of users.
+ Future<void> refreshUsers() async {
+ _accounts = List<Account>.from(await _userManager.getPreviousUsers());
+ notifyListeners();
+ }
+
+ // TODO: revert to default state when client logs out.
+ /// Permanently removes the user.
+ Future removeUser(Account account) async {
+ try {
+ await _userManager.removeUser(account.id);
+ } on UserLoginException catch (ex) {
+ log.severe(ex);
+ } finally {
+ await refreshUsers();
+ }
+ }
+
+ @override
+ // TODO: implement $serviceData
+ ServiceData get $serviceData => null;
+}
diff --git a/lib/base_shell/lib/base_shell_model.dart b/lib/base_shell/lib/base_shell_model.dart
new file mode 100644
index 0000000..aaa6435
--- /dev/null
+++ b/lib/base_shell/lib/base_shell_model.dart
@@ -0,0 +1,43 @@
+// Copyright 2017 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 'package:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
+import 'package:lib.widgets/model.dart';
+import 'package:meta/meta.dart';
+
+export 'package:lib.widgets/model.dart' show ScopedModel, ScopedModelDescendant;
+
+/// The [Model] that provides a [BaseShellContext] and [UserProvider].
+class BaseShellModel extends Model {
+ BaseShellContext _baseShellContext;
+ UserProvider _userProvider;
+ Presentation _presentation;
+
+ /// The [BaseShellContext] given to this app's [BaseShell].
+ BaseShellContext get baseShellContext => _baseShellContext;
+
+ /// The [UserProvider] given to this app's [BaseShell].
+ UserProvider get userProvider => _userProvider;
+
+ /// The [Presentation] given to this app's [BaseShell].
+ Presentation get presentation => _presentation;
+
+ /// Called when this app's [BaseShell] is given its [BaseShellContext],
+ /// and [UserProvider], and (optionally) its [Presentation].
+ @mustCallSuper
+ void onReady(
+ UserProvider userProvider,
+ BaseShellContext baseShellContext,
+ Presentation presentation,
+ ) {
+ _userProvider = userProvider;
+ _baseShellContext = baseShellContext;
+ _presentation = presentation;
+ notifyListeners();
+ }
+
+ /// Called when the app's [BaseShell] stops.
+ void onStop() => null;
+}
diff --git a/lib/base_shell/lib/base_shell_widget.dart b/lib/base_shell/lib/base_shell_widget.dart
new file mode 100644
index 0000000..11a4c28
--- /dev/null
+++ b/lib/base_shell/lib/base_shell_widget.dart
@@ -0,0 +1,103 @@
+// Copyright 2017 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 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:fuchsia_services/services.dart' show StartupContext;
+import 'package:lib.device.dart/device.dart';
+import 'package:lib.widgets/widgets.dart' show WindowMediaQuery;
+import 'package:meta/meta.dart';
+
+import 'base_shell_model.dart';
+
+/// A wrapper widget intended to be the root of the application that is
+/// a [BaseShell]. Its main purpose is to hold the [StartupContext] and
+/// [BaseShell] instances so they aren't garbage collected.
+/// For convenience, [advertise] does the advertising of the app as a
+/// [BaseShell] to the rest of the system via the [StartupContext].
+/// Also for convienence, the [BaseShellModel] given to this widget
+/// will be made available to [child] and [child]'s descendants.
+class BaseShellWidget<T extends BaseShellModel> extends StatelessWidget {
+ /// The [StartupContext] to [advertise] its [BaseShell] services to.
+ final StartupContext startupContext;
+
+ /// The bindings for the [BaseShell] service implemented by [BaseShellImpl].
+ final Set<BaseShellBinding> _baseShellBindingSet =
+ new Set<BaseShellBinding>();
+
+ /// The bindings for the [Lifecycle] service implemented by [BaseShellImpl].
+ final Set<LifecycleBinding> _lifecycleBindingSet =
+ new Set<LifecycleBinding>();
+
+ /// The [BaseShell] to [advertise].
+ final BaseShellImpl _baseShell;
+
+ /// The rest of the application.
+ final Widget child;
+
+ final T _baseShellModel;
+
+ /// Constructor.
+ BaseShellWidget({
+ @required this.startupContext,
+ T baseShellModel,
+ AuthenticationUiContext authenticationUiContext,
+ this.child,
+ }) : _baseShellModel = baseShellModel,
+ _baseShell = _createBaseShell(
+ baseShellModel,
+ authenticationUiContext,
+ );
+
+ @override
+ Widget build(BuildContext context) => new MaterialApp(
+ home: new Material(
+ child: new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new WindowMediaQuery(
+ child: _baseShellModel == null
+ ? child
+ : new ScopedModel<T>(model: _baseShellModel, child: child),
+ ),
+ ),
+ ),
+ );
+
+ /// Advertises [_baseShell] as a [BaseShell] to the rest of the system via
+ /// the [StartupContext].
+ void advertise() {
+ startupContext.outgoing
+ ..addPublicService((InterfaceRequest<BaseShell> request) {
+ BaseShellBinding binding = new BaseShellBinding()
+ ..bind(_baseShell, request);
+ _baseShellBindingSet.add(binding);
+ }, BaseShell.$serviceName)
+ ..addPublicService((InterfaceRequest<Lifecycle> request) {
+ LifecycleBinding binding = new LifecycleBinding()
+ ..bind(_baseShell, request);
+ _lifecycleBindingSet.add(binding);
+ }, Lifecycle.$serviceName);
+ }
+
+ static BaseShell _createBaseShell(
+ BaseShellModel baseShellModel,
+ AuthenticationUiContext authenticationUiContext,
+ ) {
+ return new BaseShellImpl(
+ authenticationUiContext: authenticationUiContext,
+ onReady: baseShellModel?.onReady,
+ onStop: () {
+ baseShellModel?.onStop?.call();
+ },
+ );
+ }
+
+ /// Cancels any authentication flow currently in progress.
+ void cancelAuthenticationFlow() {
+ _baseShell.closeAuthenticationUiContextBindings();
+ }
+}
diff --git a/lib/base_shell/lib/netstack_model.dart b/lib/base_shell/lib/netstack_model.dart
new file mode 100644
index 0000000..9b78922
--- /dev/null
+++ b/lib/base_shell/lib/netstack_model.dart
@@ -0,0 +1,181 @@
+// Copyright 2017 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 'dart:async';
+
+import 'package:fidl_fuchsia_net/fidl_async.dart' as net;
+import 'package:fidl_fuchsia_netstack/fidl_async.dart' as ns;
+import 'package:fidl_fuchsia_netstack/fidl_async.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/widgets.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:lib.widgets/model.dart';
+
+const String _kLoopbackInterfaceName = 'en1';
+
+const Duration _kRepeatAnimationDuration = const Duration(milliseconds: 400);
+
+const Duration _kRevealAnimationDuration = const Duration(milliseconds: 200);
+void _updateAnimations(
+ bool oldValue,
+ bool newValue,
+ AnimationController reveal,
+ AnimationController repeat,
+) {
+ if (newValue) {
+ reveal.forward();
+ } else {
+ reveal.reverse();
+ }
+ if (newValue && oldValue && !repeat.isAnimating) {
+ repeat
+ ..value = 0.0
+ ..forward();
+ }
+}
+
+/// Information about an interface.
+class InterfaceInfo {
+ /// The animation to use when revealing receiving information.
+ AnimationController receivingRevealAnimation;
+
+ /// The animation to use when repeating receiving information.
+ AnimationController receivingRepeatAnimation;
+
+ /// The animation to use when revealing sending information.
+ AnimationController sendingRevealAnimation;
+
+ /// The animation to use when repeating sending information.
+ AnimationController sendingRepeatAnimation;
+ ns.NetInterface _interface;
+ ns.NetInterfaceStats _stats;
+ bool _receiving = false;
+ bool _sending = false;
+
+ /// Constructor.
+ InterfaceInfo(this._interface, this._stats, TickerProvider _vsync) {
+ receivingRevealAnimation = new AnimationController(
+ duration: _kRevealAnimationDuration,
+ vsync: _vsync,
+ );
+ receivingRepeatAnimation = new AnimationController(
+ duration: _kRepeatAnimationDuration,
+ vsync: _vsync,
+ );
+ sendingRevealAnimation = new AnimationController(
+ duration: _kRevealAnimationDuration,
+ vsync: _vsync,
+ );
+ sendingRepeatAnimation = new AnimationController(
+ duration: _kRepeatAnimationDuration,
+ vsync: _vsync,
+ );
+ }
+
+ /// Name of the interface.
+ String get name => _interface.name;
+
+ void _update(ns.NetInterface interface, ns.NetInterfaceStats stats) {
+ _interface = interface;
+
+ bool oldReceiving = _receiving;
+ _receiving = _stats.rx.bytesTotal != stats.rx.bytesTotal;
+ _updateAnimations(
+ oldReceiving,
+ _receiving,
+ receivingRevealAnimation,
+ receivingRepeatAnimation,
+ );
+
+ bool oldSending = _sending;
+ _sending = _stats.tx.bytesTotal != stats.tx.bytesTotal;
+ _updateAnimations(
+ oldSending,
+ _sending,
+ sendingRevealAnimation,
+ sendingRepeatAnimation,
+ );
+
+ _stats = stats;
+ }
+}
+
+/// Provides netstack information.
+class NetstackModel extends Model with TickerProviderModelMixin {
+ /// The netstack containing networking information for the device.
+ final ns.NetstackProxy netstack;
+
+ StreamSubscription<bool> _reachabilitySubscription;
+ StreamSubscription<List<NetInterface>> _interfaceSubscription;
+ final net.ConnectivityProxy connectivity = net.ConnectivityProxy();
+
+ final ValueNotifier<bool> networkReachable = ValueNotifier<bool>(false);
+
+ final Map<int, InterfaceInfo> _interfaces = <int, InterfaceInfo>{};
+
+ /// Constructor.
+ NetstackModel({this.netstack}) {
+ connectToEnvironmentService(connectivity);
+ networkReachable.addListener(notifyListeners);
+ _reachabilitySubscription =
+ connectivity.onNetworkReachable.listen((reachable) {
+ networkReachable.value = reachable;
+ });
+ }
+
+ /// The current interfaces on the device.
+ List<InterfaceInfo> get interfaces => _interfaces.values.toList();
+
+ void interfacesChanged(List<ns.NetInterface> interfaces) {
+ List<ns.NetInterface> filteredInterfaces = interfaces
+ .where((ns.NetInterface interface) =>
+ interface.name != _kLoopbackInterfaceName)
+ .toList();
+
+ List<int> ids = filteredInterfaces
+ .map((ns.NetInterface interface) => interface.id)
+ .toList();
+
+ _interfaces.keys
+ .where((int id) => !ids.contains(id))
+ .toList()
+ .forEach(_interfaces.remove);
+
+ for (ns.NetInterface interface in filteredInterfaces) {
+ netstack.getStats(interface.id).then(
+ (ns.NetInterfaceStats stats) {
+ if (_interfaces[interface.id] == null) {
+ _interfaces[interface.id] = new InterfaceInfo(
+ interface,
+ stats,
+ this,
+ );
+ } else {
+ _interfaces[interface.id]._update(interface, stats);
+ }
+ notifyListeners();
+ },
+ );
+ }
+ }
+
+ /// Starts listening for netstack interfaces.
+ void start() {
+ _interfaceSubscription =
+ netstack.onInterfacesChanged.listen(interfacesChanged);
+ }
+
+ /// Stops listening for netstack interfaces.
+ void stop() {
+ if (_interfaceSubscription != null) {
+ _interfaceSubscription.cancel();
+ _interfaceSubscription = null;
+ }
+
+ if (_reachabilitySubscription != null) {
+ _reachabilitySubscription.cancel();
+ _reachabilitySubscription = null;
+ }
+ }
+}
diff --git a/lib/base_shell/lib/user_manager.dart b/lib/base_shell/lib/user_manager.dart
new file mode 100644
index 0000000..12de4c4
--- /dev/null
+++ b/lib/base_shell/lib/user_manager.dart
@@ -0,0 +1,99 @@
+import 'dart:async';
+
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:fidl_fuchsia_modular_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_viewsv1token/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+
+/// Handles adding, removing, and logging, and controlling users.
+class BaseShellUserManager {
+ final UserProvider _userProvider;
+
+ final StreamController<void> _userLogoutController =
+ StreamController<void>.broadcast();
+
+ BaseShellUserManager(this._userProvider);
+
+ Stream<void> get onLogout => _userLogoutController.stream;
+
+ /// Adds a new user, displaying UI as required.
+ ///
+ /// The UI will be displayed in the space provided to authenticationContext
+ /// in the base shell widget.
+ Future<String> addUser() {
+ final completer = Completer<String>();
+
+ _userProvider.addUser(IdentityProvider.google).then((response) {
+ if (response.errorCode == null || response.errorCode == '') {
+ completer.complete(response.account.id);
+ } else {
+ log.warning('ERROR adding user! ${response.errorCode}');
+ completer
+ .completeError(UserLoginException('addUser', response.errorCode));
+ }
+ });
+
+ return completer.future;
+ }
+
+ /// Logs in the user given by [accountId].
+ ///
+ /// Takes in [serviceProviderHandle] which gets passed to the session shell.
+ /// Returns a handle to the [ViewOwner] that the base shell should use
+ /// to open a [ChildViewConnection] to display the session shell.
+ InterfaceHandle<ViewOwner> login(String accountId,
+ InterfaceHandle<ServiceProvider> serviceProviderHandle) {
+ final InterfacePair<ViewOwner> viewOwner = InterfacePair<ViewOwner>();
+ final UserLoginParams params = UserLoginParams(
+ accountId: accountId,
+ viewOwner: viewOwner.passRequest(),
+ services: serviceProviderHandle,
+ );
+
+ _userProvider.login(params);
+
+ return viewOwner.passHandle();
+ }
+
+ Future<void> removeUser(String userId) {
+ final completer = Completer<void>();
+
+ _userProvider.removeUser(userId).then((errorCode) {
+ if (errorCode != null && errorCode != '') {
+ completer
+ .completeError(UserLoginException('removing $userId', errorCode));
+ }
+ completer.complete();
+ });
+
+ return completer.future;
+ }
+
+ /// Gets the list of accounts already logged in.
+ Future<Iterable<Account>> getPreviousUsers() {
+ final completer = Completer<Iterable<Account>>();
+
+ _userProvider.previousUsers().then(completer.complete);
+
+ return completer.future;
+ }
+
+ void close() {
+ _userLogoutController.close();
+ }
+}
+
+/// Exception thrown when performing user management operations.
+class UserLoginException implements Exception {
+ final String errorCode;
+ final String operation;
+
+ UserLoginException(this.operation, this.errorCode);
+
+ @override
+ String toString() {
+ return 'Failed during $operation: $errorCode';
+ }
+}
diff --git a/lib/base_shell/pubspec.yaml b/lib/base_shell/pubspec.yaml
new file mode 100644
index 0000000..2a2ea63
--- /dev/null
+++ b/lib/base_shell/pubspec.yaml
@@ -0,0 +1,8 @@
+# Copyright 2018 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.
+
+name: base_shell
+dependencies:
+ flutter:
+ sdk: flutter
\ No newline at end of file
diff --git a/packages/prod/BUILD.gn b/packages/prod/BUILD.gn
index a77b2e4..8017175 100644
--- a/packages/prod/BUILD.gn
+++ b/packages/prod/BUILD.gn
@@ -70,6 +70,7 @@
"//topaz/packages/prod:system_dashboard",
"//topaz/packages/prod:term",
"//topaz/packages/prod:text_input_mod",
+ "//topaz/packages/prod:userpicker_base_shell",
"//topaz/packages/prod:web_runner",
"//topaz/packages/prod:wifi_settings",
"//topaz/packages/prod:xi",
@@ -148,6 +149,14 @@
]
}
+group("userpicker_base_shell") {
+ testonly = true
+ public_deps = [
+ "//topaz/packages/prod:flutter",
+ "//topaz/bin/userpicker_base_shell",
+ ]
+}
+
group("dart_aot_runner") {
testonly = true
public_deps = [
@@ -249,12 +258,12 @@
group("ermine") {
testonly = true
public_deps = [
- "//peridot/packages/prod:dev_base_shell",
"//topaz/packages/prod:chromium",
"//topaz/packages/prod:dart_jit_runner",
"//topaz/packages/prod:dart_jit_product_runner",
"//topaz/packages/prod:google_auth_provider",
"//topaz/packages/prod:mondrian",
+ "//topaz/packages/prod:userpicker_base_shell",
"//topaz/packages/prod:wifi_settings",
"//topaz/shell/ermine:ermine",
"//topaz/shell/ermine:ermine_ask_module",
diff --git a/shell/ermine/config/basemgr.config b/shell/ermine/config/basemgr.config
index c8fa45d..a131f3d 100644
--- a/shell/ermine/config/basemgr.config
+++ b/shell/ermine/config/basemgr.config
@@ -1,7 +1,7 @@
{
"apps": [
[ "fuchsia-pkg://fuchsia.com/basemgr#meta/basemgr.cmx",
- "--base_shell=fuchsia-pkg://fuchsia.com/dev_base_shell#meta/dev_base_shell.cmx",
+ "--base_shell=fuchsia-pkg://fuchsia.com/userpicker_base_shell#meta/userpicker_base_shell.cmx",
"--sessionmgr_args=--startup_agents=fuchsia-pkg://fuchsia.com/experiment_agent#meta/experiment_agent.cmx\\"]
]
}