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\\"]
   ]
 }