[login_shell] move userpicker_base_shell

Moves the userpicker_bash_shell out of topaz
and into experiences. This is an initial step of a
soft transition which just puts the code in place.
There are no references to this location in the
tree so the workstation build will still compile.

Change-Id: Idc920def9796cfdfcbeb2ca17cf777697091324f
diff --git a/session_shells/ermine/login_shell/BUILD.gn b/session_shells/ermine/login_shell/BUILD.gn
new file mode 100644
index 0000000..447d7ad
--- /dev/null
+++ b/session_shells/ermine/login_shell/BUILD.gn
@@ -0,0 +1,50 @@
+# 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("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+flutter_app("userpicker_base_shell") {
+  main_dart = "lib/main.dart"
+
+  meta = [
+    {
+      path = rebase_path("meta/userpicker_base_shell.cmx")
+      dest = "userpicker_base_shell.cmx"
+    },
+  ]
+
+  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",
+  ]
+
+  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/fuchsia_scenic_flutter",
+    "//topaz/public/dart/widgets:lib.widgets",
+    "//topaz/public/lib/device/dart",
+    "//zircon/system/fidl/fuchsia-device-manager",
+  ]
+}
diff --git a/session_shells/ermine/login_shell/README.md b/session_shells/ermine/login_shell/README.md
new file mode 100644
index 0000000..76b12ff
--- /dev/null
+++ b/session_shells/ermine/login_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/session_shells/ermine/login_shell/analysis_options.yaml b/session_shells/ermine/login_shell/analysis_options.yaml
new file mode 100644
index 0000000..54917c0
--- /dev/null
+++ b/session_shells/ermine/login_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/session_shells/ermine/login_shell/lib/authentication_overlay.dart b/session_shells/ermine/login_shell/lib/authentication_overlay.dart
new file mode 100644
index 0000000..ae4c170
--- /dev/null
+++ b/session_shells/ermine/login_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) =>
+      ScopedModelDescendant<AuthenticationOverlayModel>(
+        builder: (
+          BuildContext context,
+          Widget child,
+          AuthenticationOverlayModel model,
+        ) =>
+            AnimatedBuilder(
+              animation: model.animation,
+              builder: (BuildContext context, Widget child) => Offstage(
+                    offstage: model.animation.isDismissed,
+                    child: Opacity(
+                      opacity: model.animation.value,
+                      child: child,
+                    ),
+                  ),
+              child: Stack(
+                fit: StackFit.passthrough,
+                children: <Widget>[
+                  GestureDetector(
+                    onTap: _onCancel,
+                  ),
+                  FractionallySizedBox(
+                    widthFactor: 0.75,
+                    heightFactor: 0.75,
+                    child: ChildView(
+                      connection: model.childViewConnection,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+      );
+}
diff --git a/session_shells/ermine/login_shell/lib/authentication_overlay_model.dart b/session_shells/ermine/login_shell/lib/authentication_overlay_model.dart
new file mode 100644
index 0000000..ac146ae
--- /dev/null
+++ b/session_shells/ermine/login_shell/lib/authentication_overlay_model.dart
@@ -0,0 +1,62 @@
+// 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_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 = AnimationController(
+      value: 0.0,
+      duration: Duration(seconds: 1),
+      vsync: this,
+    );
+    _curvedTransitionAnimation = 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) {
+        connection.requestFocus();
+      },
+      onUnavailable: (ChildViewConnection connection) {
+        _transitionAnimation.reverse();
+      },
+    );
+    _transitionAnimation.forward();
+    notifyListeners();
+  }
+
+  /// Stops showing a previously started overlay.
+  void onStopOverlay() {
+    _transitionAnimation.reverse();
+    notifyListeners();
+  }
+
+  @override
+  Ticker createTicker(TickerCallback onTick) => Ticker(onTick);
+}
diff --git a/session_shells/ermine/login_shell/lib/authentication_ui_context_impl.dart b/session_shells/ermine/login_shell/lib/authentication_ui_context_impl.dart
new file mode 100644
index 0000000..1c9ad39
--- /dev/null
+++ b/session_shells/ermine/login_shell/lib/authentication_ui_context_impl.dart
@@ -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 'package:fidl_fuchsia_auth/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:flutter/widgets.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(ViewHolderToken viewHolderToken) async  =>
+      _onStartOverlay?.call(viewHolderToken);
+
+  @override
+  Future<void> stopOverlay() async => _onStopOverlay?.call();
+}
diff --git a/session_shells/ermine/login_shell/lib/circular_button.dart b/session_shells/ermine/login_shell/lib/circular_button.dart
new file mode 100644
index 0000000..9ff5eae
--- /dev/null
+++ b/session_shells/ermine/login_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) => Material(
+        type: MaterialType.circle,
+        elevation: 2.0,
+        color: Colors.grey[200],
+        child: InkWell(
+          onTap: () => onTap?.call(),
+          child: Container(
+            padding: EdgeInsets.all(12.0),
+            child: Icon(icon),
+          ),
+        ),
+      );
+}
diff --git a/session_shells/ermine/login_shell/lib/clock.dart b/session_shells/ermine/login_shell/lib/clock.dart
new file mode 100644
index 0000000..98c04d7
--- /dev/null
+++ b/session_shells/ermine/login_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 = TimeStringer();
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        return AnimatedBuilder(
+          animation: _time,
+          builder: (BuildContext context, Widget child) {
+            return Container(
+              child: Text(
+                _time.timeOnly,
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: min(
+                    constraints.maxWidth / 6.0,
+                    constraints.maxHeight / 6.0,
+                  ),
+                  fontWeight: FontWeight.w200,
+                  letterSpacing: 4.0,
+                ),
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/session_shells/ermine/login_shell/lib/main.dart b/session_shells/ermine/login_shell/lib/main.dart
new file mode 100644
index 0000000..8db8b1c
--- /dev/null
+++ b/session_shells/ermine/login_shell/lib/main.dart
@@ -0,0 +1,172 @@
+// 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_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:fidl_fuchsia_sys/fidl_async.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;
+
+/// The main base shell widget.
+BaseShellWidget<UserPickerBaseShellModel> _baseShellWidget;
+
+void main() {
+  setupLogger(name: 'userpicker_base_shell');
+  StartupContext startupContext = StartupContext.fromStartupInfo();
+  final launcherProxy = LauncherProxy();
+  startupContext.incoming.connectToService(launcherProxy);
+  
+  NetstackProxy netstackProxy = NetstackProxy();
+  startupContext.incoming.connectToService(netstackProxy);
+  
+  NetstackModel netstackModel = NetstackModel(netstack: netstackProxy)..start();
+
+  _OverlayModel wifiInfoOverlayModel = _OverlayModel();
+
+  final AuthenticationOverlayModel authModel = AuthenticationOverlayModel();
+
+  UserPickerBaseShellModel userPickerBaseShellModel = UserPickerBaseShellModel(
+    onBaseShellStopped: () {
+      netstackProxy.ctrl.close();
+      netstackModel.dispose();
+    },
+    onLogin: () {
+      wifiInfoOverlayModel.showing = false;
+    },
+    onWifiTapped: () {
+      wifiInfoOverlayModel.showing = !wifiInfoOverlayModel.showing;
+    },
+  );
+
+  Widget mainWidget = Stack(
+    fit: StackFit.passthrough,
+    children: <Widget>[
+      UserPickerBaseShellScreen(
+        launcher: launcherProxy,
+      ),
+      ScopedModel<AuthenticationOverlayModel>(
+        model: authModel,
+        child: AuthenticationOverlay(),
+      ),
+    ],
+  );
+
+  Widget app = mainWidget;
+
+  List<OverlayEntry> overlays = <OverlayEntry>[
+    OverlayEntry(
+      builder: (BuildContext context) => MediaQuery(
+            data: MediaQueryData(),
+            child: FocusScope(
+              node: FocusScopeNode(),
+              autofocus: true,
+              child: app,
+            ),
+          ),
+    ),
+    OverlayEntry(
+      builder: (BuildContext context) => ScopedModel<_OverlayModel>(
+            model: wifiInfoOverlayModel,
+            child: _WifiInfo(
+              wifiWidget: ApplicationWidget(
+                url:
+                    'fuchsia-pkg://fuchsia.com/wifi_settings#meta/wifi_settings.cmx',
+                launcher: launcherProxy,
+              ),
+            ),
+          ),
+    ),
+  ];
+
+  _baseShellWidget = BaseShellWidget<UserPickerBaseShellModel>(
+    startupContext: startupContext,
+    baseShellModel: userPickerBaseShellModel,
+    authenticationUiContext: AuthenticationUiContextImpl(
+        onStartOverlay: authModel.onStartOverlay,
+        onStopOverlay: authModel.onStopOverlay),
+    child: LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) =>
+          (constraints.biggest == Size.zero)
+              ? const Offstage()
+              : ScopedModel<NetstackModel>(
+                  model: netstackModel,
+                  child: 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) => ScopedModelDescendant<_OverlayModel>(
+        builder: (
+          BuildContext context,
+          Widget child,
+          _OverlayModel model,
+        ) =>
+            Offstage(
+              offstage: !model.showing,
+              child: Stack(
+                children: <Widget>[
+                  Listener(
+                    behavior: HitTestBehavior.opaque,
+                    onPointerDown: (PointerDownEvent event) {
+                      model.showing = false;
+                    },
+                  ),
+                  Center(
+                    child: FractionallySizedBox(
+                      widthFactor: 0.75,
+                      heightFactor: 0.75,
+                      child: Container(
+                        margin: EdgeInsets.all(8.0),
+                        child: PhysicalModel(
+                          color: Colors.grey[900],
+                          elevation: _kIndicatorElevation,
+                          borderRadius: 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/session_shells/ermine/login_shell/lib/time_stringer.dart b/session_shells/ermine/login_shell/lib/time_stringer.dart
new file mode 100644
index 0000000..b35a06d
--- /dev/null
+++ b/session_shells/ermine/login_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 = DateFormat('h:mm', 'en_US');
+final DateFormat _kDateOnlyDateFormat = DateFormat('EEEE MMM d', 'en_US');
+final DateFormat _kShortStringDateFormat = DateFormat('h:mm', 'en_US');
+final DateFormat _kLongStringDateFormat = DateFormat('EEEE h:mm', 'en_US');
+final DateFormat _kMeridiemOnlyFormat = 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(
+        DateTime.now(),
+      )
+      .toUpperCase();
+
+  /// Returns the date only (eg. 'MONDAY AUG 3').
+  String get dateOnly => _kDateOnlyDateFormat
+      .format(
+        DateTime.now(),
+      )
+      .toUpperCase();
+
+  /// Returns a short version of the time (eg. '10:34').
+  String get shortString =>
+      _kShortStringDateFormat.format(DateTime.now()).toLowerCase();
+
+  /// Returns a long version of the time including the day (eg. 'Monday 10:34').
+  String get longString =>
+      _kLongStringDateFormat.format(DateTime.now()).toLowerCase();
+
+  /// Returns the meridiem (eg. 'AM')
+  String get meridiem =>
+      _kMeridiemOnlyFormat.format(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 =
+        Timer(Duration(seconds: 61 - DateTime.now().second), () {
+      _notifyListeners();
+      _scheduleTimer();
+    });
+  }
+
+  void _notifyListeners() {
+    for (VoidCallback listener in _listeners.toList()) {
+      listener();
+    }
+  }
+}
diff --git a/session_shells/ermine/login_shell/lib/user_list.dart b/session_shells/ermine/login_shell/lib/user_list.dart
new file mode 100644
index 0000000..919e021
--- /dev/null
+++ b/session_shells/ermine/login_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 = TextStyle(
+  color: Colors.white,
+  fontSize: 10.0,
+  letterSpacing: 1.0,
+  fontWeight: FontWeight.w300,
+);
+
+final BorderRadius _kButtonBorderRadiusPhone =
+    BorderRadius.circular(_kUserAvatarSizeSmall / 2.0);
+final BorderRadius _kButtonBorderRadiusLarge =
+    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 GestureDetector(
+      onTap: () => onTap?.call(),
+      child: Container(
+        height: size,
+        width: size,
+        child: 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: Center(
+        child: Icon(
+          icon,
+          color: Colors.white,
+          size: size / 2.0,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildUserActionButton({
+    Widget child,
+    VoidCallback onTap,
+    bool isSmall,
+    double width,
+    bool isDisabled = false,
+  }) {
+    return GestureDetector(
+      onTap: isDisabled ? null : () => onTap?.call(),
+      child: Container(
+        height: isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge,
+        width: width ?? (isSmall ? _kButtonWidthSmall : _kButtonWidthLarge),
+        alignment: FractionalOffset.center,
+        margin: EdgeInsets.only(left: 16.0),
+        decoration: BoxDecoration(
+          borderRadius:
+              isSmall ? _kButtonBorderRadiusPhone : _kButtonBorderRadiusLarge,
+          border: 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 Row(
+        children: <Widget>[
+          _buildUserActionButton(
+            child: Text(
+              'RESET',
+              style: TextStyle(
+                fontSize: fontSize,
+                color: Colors.white,
+              ),
+            ),
+            onTap: model.resetTapped,
+            isSmall: isSmall,
+          ),
+          _buildUserActionButton(
+            child: Text(
+              'WIFI',
+              style: TextStyle(
+                fontSize: fontSize,
+                color: Colors.white,
+              ),
+            ),
+            onTap: model.wifiTapped,
+            isSmall: isSmall,
+          ),
+          _buildUserActionButton(
+            child: Text(
+              'LOGIN DISABLED => No SessionShell configured',
+              style: TextStyle(
+                fontSize: fontSize,
+                color: Colors.white,
+              ),
+            ),
+            onTap: () {},
+            isSmall: isSmall,
+            isDisabled: true,
+          ),
+        ],
+      );
+    }
+    return Row(
+      children: <Widget>[
+        _buildIconButton(
+          onTap: () => model.hideUserActions(),
+          isSmall: isSmall,
+          icon: Icons.close,
+        ),
+        _buildUserActionButton(
+          child: Text(
+            'RESET',
+            style: TextStyle(
+              fontSize: fontSize,
+              color: Colors.white,
+            ),
+          ),
+          onTap: model.resetTapped,
+          isSmall: isSmall,
+        ),
+        _buildUserActionButton(
+          child: Text(
+            'WIFI',
+            style: TextStyle(
+              fontSize: fontSize,
+              color: Colors.white,
+            ),
+          ),
+          onTap: model.wifiTapped,
+          isSmall: isSmall,
+        ),
+        _buildUserActionButton(
+          child: Text(
+            'LOGIN',
+            style: TextStyle(
+              fontSize: fontSize,
+              color: Colors.white,
+            ),
+          ),
+          onTap: () {
+            model
+              ..createAndLoginUser()
+              ..hideUserActions();
+          },
+          isSmall: isSmall,
+        ),
+        _buildUserActionButton(
+          child: Text(
+            'GUEST',
+            style: 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 = 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 = LongPressDraggable<Account>(
+      child: userCard,
+      feedback: userCard,
+      data: account,
+      childWhenDragging: 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 Padding(
+        padding: EdgeInsets.only(left: 16.0),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: <Widget>[
+            Padding(
+              padding: EdgeInsets.only(bottom: 8.0),
+              child: Text(
+                account.displayName,
+                textAlign: TextAlign.center,
+                maxLines: 1,
+                style: _kTextStyle,
+              ),
+            ),
+            userImage,
+          ],
+        ),
+      );
+    } else {
+      return Padding(
+        padding: EdgeInsets.only(left: 16.0),
+        child: userImage,
+      );
+    }
+  }
+
+  Widget _buildUserList(UserPickerBaseShellModel model) {
+    return 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(
+            Align(
+              alignment: FractionalOffset.bottomCenter,
+              child: _buildExpandedUserActions(
+                model: model,
+                isSmall: isSmall,
+              ),
+            ),
+          );
+        } else {
+          children.add(
+            Align(
+              alignment: FractionalOffset.bottomCenter,
+              child: _buildIconButton(
+                onTap: model.showUserActions,
+                isSmall: isSmall,
+                icon: Icons.add,
+              ),
+            ),
+          );
+        }
+
+        children.addAll(
+          model.accounts.map(
+            (Account account) => Align(
+                  alignment: FractionalOffset.bottomCenter,
+                  child: _buildUserEntry(
+                    account: account,
+                    onTap: () {
+                      model
+                        ..login(account.id)
+                        ..hideUserActions();
+                    },
+                    isSmall: isSmall,
+                    model: model,
+                  ),
+                ),
+          ),
+        );
+
+        return Container(
+          height: (isSmall ? _kUserAvatarSizeSmall : _kUserAvatarSizeLarge) +
+              24.0 +
+              (model.showingUserActions ? 24.0 : 0.0),
+          child: AnimatedOpacity(
+            duration: Duration(milliseconds: 250),
+            opacity: model.showingRemoveUserTarget ? 0.0 : 1.0,
+            child: ListView(
+              padding: EdgeInsets.only(
+                bottom: 24.0,
+                right: 24.0,
+              ),
+              scrollDirection: Axis.horizontal,
+              reverse: true,
+              physics: BouncingScrollPhysics(),
+              shrinkWrap: true,
+              children: children,
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) =>
+      ScopedModelDescendant<UserPickerBaseShellModel>(builder: (
+        BuildContext context,
+        Widget child,
+        UserPickerBaseShellModel model,
+      ) {
+        if (model.showingLoadingSpinner) {
+          return Stack(
+            fit: StackFit.passthrough,
+            children: <Widget>[
+              Center(
+                child: Container(
+                  width: 64.0,
+                  height: 64.0,
+                  child: FuchsiaSpinner(),
+                ),
+              ),
+            ],
+          );
+        } else {
+          return Stack(
+            fit: StackFit.passthrough,
+            children: <Widget>[
+              _buildUserList(model),
+            ],
+          );
+        }
+      });
+}
diff --git a/session_shells/ermine/login_shell/lib/user_picker_base_shell_model.dart b/session_shells/ermine/login_shell/lib/user_picker_base_shell_model.dart
new file mode 100644
index 0000000..decbdb0
--- /dev/null
+++ b/session_shells/ermine/login_shell/lib/user_picker_base_shell_model.dart
@@ -0,0 +1,201 @@
+// 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/fidl.dart';
+import 'package:fidl_fuchsia_cobalt/fidl_async.dart' as cobalt;
+import 'package:fidl_fuchsia_device_manager/fidl_async.dart' as devmgr;
+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';
+import 'package:zircon/zircon.dart';
+import 'package:fuchsia_logger/logger.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 = 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() async {
+      final ChannelPair channels = ChannelPair();
+      if (channels.status != 0) {
+        log.severe('Unable to create channels: $channels.status');
+        return;
+      }
+
+      int status = System.connectToService(
+        '/svc/${devmgr.Administrator.$serviceName}',
+        channels.second.passHandle());
+      if (status != 0 ) {
+        channels.first.close();
+        log.severe('Unable to connect to device administrator service: $status');
+        return;
+      }
+
+      final devmgr.AdministratorProxy admin = devmgr.AdministratorProxy();
+      admin.ctrl.bind(InterfaceHandle<devmgr.Administrator>(channels.first));
+
+      status = await admin.suspend(devmgr.suspendFlagReboot);
+      if (status != 0) {
+        log.severe('Reboot call failed with status: $status');
+      }
+
+      admin.ctrl.close();
+  }
+
+  /// 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;
+
+  /// 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 = Timer(
+          _kShowLoadingSpinnerDelay,
+          () {
+            _showingLoadingSpinner = true;
+            _showLoadingSpinnerTimer = null;
+            notifyListeners();
+          },
+        );
+      }
+    } else {
+      _showLoadingSpinnerTimer?.cancel();
+      _showLoadingSpinnerTimer = null;
+      _showingLoadingSpinner = false;
+      notifyListeners();
+    }
+  }
+}
diff --git a/session_shells/ermine/login_shell/lib/user_picker_base_shell_screen.dart b/session_shells/ermine/login_shell/lib/user_picker_base_shell_screen.dart
new file mode 100644
index 0000000..61e9947
--- /dev/null
+++ b/session_shells/ermine/login_shell/lib/user_picker_base_shell_screen.dart
@@ -0,0 +1,45 @@
+// 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: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 ScopedModelDescendant<UserPickerBaseShellModel>(
+      builder: (
+        BuildContext context,
+        Widget child,
+        UserPickerBaseShellModel model,
+      ) {
+        List<Widget> stackChildren = <Widget>[
+          UserPickerScreen(),
+          Align(
+            alignment: FractionalOffset.center,
+            child: Offstage(
+              offstage: !model.showingClock,
+              child: Clock(),
+            ),
+          ),
+        ];
+
+        return Stack(fit: StackFit.expand, children: stackChildren);
+      },
+    );
+  }
+}
diff --git a/session_shells/ermine/login_shell/lib/user_picker_screen.dart b/session_shells/ermine/login_shell/lib/user_picker_screen.dart
new file mode 100644
index 0000000..144c05f
--- /dev/null
+++ b/session_shells/ermine/login_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 ScopedModelDescendant<UserPickerBaseShellModel>(
+      builder: (
+        BuildContext context,
+        Widget child,
+        UserPickerBaseShellModel model,
+      ) {
+        return Material(
+          color: Colors.grey[900],
+          child: Stack(
+            fit: StackFit.passthrough,
+            children: <Widget>[
+              /// Add user picker for selecting users and adding new users
+              Align(
+                alignment: FractionalOffset.bottomRight,
+                child: RepaintBoundary(
+                  child: UserList(
+                    loginDisabled: false,
+                  ),
+                ),
+              ),
+
+              // Add user removal target
+              Align(
+                alignment: FractionalOffset.center,
+                child: RepaintBoundary(
+                  child: Container(
+                    child: DragTarget<Account>(
+                      onWillAccept: (Account data) => true,
+                      onAccept: model.removeUser,
+                      builder: (
+                        _,
+                        List<Account> candidateData,
+                        __,
+                      ) =>
+                          _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() => _UserRemovalTargetState();
+}
+
+class _UserRemovalTargetState extends State<_UserRemovalTarget>
+    with TickerProviderStateMixin {
+  AnimationController _masterAnimationController;
+  AnimationController _initialScaleController;
+  CurvedAnimation _initialScaleCurvedAnimation;
+  AnimationController _scaleController;
+  CurvedAnimation _scaleCurvedAnimation;
+
+  @override
+  void initState() {
+    super.initState();
+    _masterAnimationController = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 500),
+    );
+    _initialScaleController = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 250),
+    );
+    _initialScaleCurvedAnimation = CurvedAnimation(
+      parent: _initialScaleController,
+      curve: Curves.fastOutSlowIn,
+      reverseCurve: Curves.fastOutSlowIn,
+    );
+    _scaleController = AnimationController(
+      vsync: this,
+      duration: Duration(milliseconds: 250),
+    );
+    _scaleCurvedAnimation = 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) => Container(
+        child: AnimatedBuilder(
+          animation: _masterAnimationController,
+          builder: (BuildContext context, Widget child) => Transform(
+                alignment: FractionalOffset.center,
+                transform: Matrix4.identity().scaled(
+                  lerpDouble(1.0, 0.7, _scaleCurvedAnimation.value) *
+                      _initialScaleCurvedAnimation.value,
+                  lerpDouble(1.0, 0.7, _scaleCurvedAnimation.value) *
+                      _initialScaleCurvedAnimation.value,
+                ),
+                child: Container(
+                  width: _kRemovalTargetSize,
+                  height: _kRemovalTargetSize,
+                  decoration: BoxDecoration(
+                    borderRadius:
+                        BorderRadius.circular(_kRemovalTargetSize / 2.0),
+                    border: Border.all(color: Colors.white.withAlpha(200)),
+                    color: Colors.white.withAlpha(
+                        lerpDouble(0, 100.0, _scaleCurvedAnimation.value)
+                            .toInt()),
+                  ),
+                  child: Center(
+                    child: Text(
+                      'REMOVE',
+                      style: TextStyle(
+                        color: Colors.white,
+                        fontSize: 16.0,
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+        ),
+      );
+}
diff --git a/session_shells/ermine/login_shell/meta/userpicker_base_shell.cmx b/session_shells/ermine/login_shell/meta/userpicker_base_shell.cmx
new file mode 100644
index 0000000..344a16c
--- /dev/null
+++ b/session_shells/ermine/login_shell/meta/userpicker_base_shell.cmx
@@ -0,0 +1,24 @@
+{
+    "program": {
+        "data": "data/userpicker_base_shell"
+    },
+    "sandbox": {
+        "dev": [
+            "misc"
+        ],
+        "services": [
+            "fuchsia.cobalt.LoggerFactory",
+            "fuchsia.device.manager.Administrator",
+            "fuchsia.fonts.Provider",
+            "fuchsia.logger.LogSink",
+            "fuchsia.modular.Clipboard",
+            "fuchsia.net.Connectivity",
+            "fuchsia.netstack.Netstack",
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.ui.input.ImeService",
+            "fuchsia.ui.policy.Presenter",
+            "fuchsia.ui.scenic.Scenic"
+        ]
+    }
+}
diff --git a/session_shells/ermine/login_shell/pubspec.yaml b/session_shells/ermine/login_shell/pubspec.yaml
new file mode 100644
index 0000000..76bca92
--- /dev/null
+++ b/session_shells/ermine/login_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