[base_shell] Move copy of model and widget to base_shell library.

Test: built.

Change-Id: I0ac5f5d5c46dc3fd1f5816fec154d4a469e8e1c7
diff --git a/lib/base_shell/lib/base_model_2.dart b/lib/base_shell/lib/base_model_2.dart
new file mode 100644
index 0000000..958e179
--- /dev/null
+++ b/lib/base_shell/lib/base_model_2.dart
@@ -0,0 +1,387 @@
+// 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.dart' as cobalt;
+import 'package:fidl_fuchsia_modular/fidl.dart';
+import 'package:fidl_fuchsia_modular_auth/fidl.dart';
+import 'package:fidl_fuchsia_netstack/fidl.dart';
+import 'package:fidl_fuchsia_sys/fidl.dart';
+import 'package:fidl_fuchsia_ui_gfx/fidl.dart';
+import 'package:fidl_fuchsia_ui_input/fidl.dart' as input;
+import 'package:fidl_fuchsia_ui_policy/fidl.dart';
+import 'package:lib.app.dart/app.dart' as app;
+import 'package:lib.app.dart/logging.dart';
+import 'package:lib.ui.flutter/child_view.dart';
+import 'package:meta/meta.dart';
+import 'package:zircon/zircon.dart' show Channel;
+
+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;
+
+/// Provides common features needed by all base shells.
+///
+/// This includes user management, presentation handling,
+/// and keyboard shortcuts.
+class CommonBaseShellModel extends BaseShellModel
+    implements
+        Presentation,
+        ServiceProvider,
+        KeyboardCaptureListenerHack,
+        PointerCaptureListenerHack,
+        PresentationModeListener {
+  /// 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>[];
+
+  /// Constructor
+  CommonBaseShellModel(this.logger) : super();
+
+  List<Account> get accounts => _accounts;
+
+  /// Returns the authenticated child view connection
+  ChildViewConnection get childViewConnection => _childViewConnection;
+
+  @override
+  void captureKeyboardEventHack(input.KeyboardEvent eventToCapture,
+      InterfaceHandle<KeyboardCaptureListenerHack> listener) {
+    presentation.captureKeyboardEventHack(eventToCapture, listener);
+  }
+
+  @override
+  void capturePointerEventsHack(
+      InterfaceHandle<PointerCaptureListenerHack> listener) {
+    presentation.capturePointerEventsHack(listener);
+  }
+
+  // |ServiceProvider|.
+  @override
+  void connectToService(String serviceName, Channel channel) {
+    // TODO(SCN-595) mozart.Presentation is being renamed to ui.Presentation.
+    if (serviceName == 'mozart.Presentation' ||
+        serviceName == 'ui.Presentation') {
+      _presentationBindings.add(PresentationBinding()
+        ..bind(this, InterfaceRequest<Presentation>(channel)));
+    } else {
+      log.warning(
+          'UserPickerBaseShell: received request for unknown service: $serviceName !');
+      channel.close();
+    }
+  }
+
+  /// 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();
+    }
+  }
+
+  @override
+  // ignore: avoid_positional_boolean_parameters
+  void enableClipping(bool enabled) {
+    presentation.enableClipping(enabled);
+  }
+
+  /// |Presentation|.
+  @override
+  void getPresentationMode(GetPresentationModeCallback callback) {
+    presentation.getPresentationMode(callback);
+  }
+
+  /// 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;
+    }
+
+    trace('waiting for internet connection');
+
+    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'});
+    logger.startTimer(
+      _kSessionShellLoginTimeMetricId,
+      0,
+      '',
+      'session_shell_login_timer_id',
+      DateTime.now().millisecondsSinceEpoch,
+      _kCobaltTimerTimeout.inSeconds,
+      (cobalt.Status 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(
+      viewOwnerHandle,
+      onAvailable: (ChildViewConnection connection) {
+        trace('session shell available');
+        log.info('BaseShell: Child view connection available!');
+        connection.requestFocus();
+        notifyListeners();
+      },
+      onUnavailable: (ChildViewConnection connection) {
+        trace('BaseShell: Child view connection now unavailable!');
+        log.info('BaseShell: Child view connection now unavailable!');
+        onLogout();
+        notifyListeners();
+      },
+    );
+    notifyListeners();
+  }
+
+  /// Called when the the session shell logs out.
+  @mustCallSuper
+  Future<void> onLogout() async {
+    trace('logout');
+    _childViewConnection = null;
+    _serviceProviderBinding.close();
+    for (PresentationBinding presentationBinding in _presentationBindings) {
+      presentationBinding.close();
+    }
+    await refreshUsers();
+    notifyListeners();
+  }
+
+  /// |PresentationModeListener|.
+  @override
+  void onModeChanged() {
+    getPresentationMode((PresentationMode mode) {
+      log.info('Presentation mode changed to: $mode');
+      switch (mode) {
+        case PresentationMode.tent:
+          setDisplayRotation(180.0, true);
+          break;
+        case PresentationMode.tablet:
+          // TODO(sanjayc): Figure out up/down orientation.
+          setDisplayRotation(90.0, true);
+          break;
+        case PresentationMode.laptop:
+        default:
+          setDisplayRotation(0.0, true);
+          break;
+      }
+    });
+  }
+
+  /// |KeyboardCaptureListener|.
+  @override
+  void onEvent(input.KeyboardEvent ev) {}
+
+  /// |PointerCaptureListener|.
+  @override
+  void onPointerEvent(input.PointerEvent event) {}
+
+  // |Presentation|.
+  // Delegate to the Presentation received by BaseShell.Initialize().
+  // 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.connectToService(
+        app.StartupContext.fromStartupInfo().environmentServices,
+        netstackProxy.ctrl);
+    _netstackModel = NetstackModel(netstack: netstackProxy)..start();
+
+    presentation
+      ..capturePointerEventsHack(_pointerCaptureListenerBinding.wrap(this))
+      ..setPresentationModeListener(
+          _presentationModeListenerBinding.wrap(this));
+
+    _userManager = BaseShellUserManager(userProvider);
+
+    _userManager.onLogout.listen((_) {
+      logger.endTimer(
+        'session_shell_log_out_timer_id',
+        DateTime.now().millisecondsSinceEpoch,
+        _kCobaltTimerTimeout.inSeconds,
+        (cobalt.Status 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!');
+      onLogout();
+    });
+
+    await refreshUsers();
+  }
+
+  // |Presentation|.
+  // Delegate to the Presentation received by BaseShell.Initialize().
+  // 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();
+  }
+
+  // |Presentation|.
+  // Delegate to the Presentation received by BaseShell.Initialize().
+  // 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();
+  }
+
+  // |Presentation|.
+  // Delegate to the Presentation received by BaseShell.Initialize().
+  // 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();
+    }
+  }
+
+  // |Presentation|.
+  @override
+  // ignore: avoid_positional_boolean_parameters
+  void setDisplayRotation(double displayRotationDegrees, bool animate) {
+    presentation.setDisplayRotation(displayRotationDegrees, animate);
+  }
+
+  // |Presentation|.
+  @override
+  void setDisplaySizeInMm(num widthInMm, num heightInMm) {
+    presentation.setDisplaySizeInMm(widthInMm, heightInMm);
+  }
+
+  // |Presentation|.
+  @override
+  void setDisplayUsage(DisplayUsage usage) {
+    presentation.setDisplayUsage(usage);
+  }
+
+  // |Presentation|.
+  /// |Presentation|.
+  @override
+  void setPresentationModeListener(
+      InterfaceHandle<PresentationModeListener> listener) {
+    presentation.setPresentationModeListener(listener);
+  }
+
+  // |Presentation|.
+  @override
+  void setRendererParams(List<RendererParam> params) {
+    presentation.setRendererParams(params);
+  }
+
+  @override
+  void useOrthographicView() {
+    presentation.useOrthographicView();
+  }
+
+  @override
+  void usePerspectiveView() {
+    presentation.usePerspectiveView();
+  }
+}
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..628bec1
--- /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.dart';
+import 'package:fidl_fuchsia_ui_policy/fidl.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..cf35f14
--- /dev/null
+++ b/lib/base_shell/lib/base_shell_widget.dart
@@ -0,0 +1,104 @@
+// 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:lib.app.dart/app.dart';
+import 'package:fidl_fuchsia_auth/fidl.dart';
+import 'package:fidl_fuchsia_modular/fidl.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:fidl/fidl.dart';
+import 'package:lib.app.dart/app.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.outgoingServices
+      ..addServiceForName((InterfaceRequest<BaseShell> request) {
+        BaseShellBinding binding = new BaseShellBinding()
+          ..bind(_baseShell, request);
+        _baseShellBindingSet.add(binding);
+      }, BaseShell.$serviceName)
+      ..addServiceForName((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();
+  }
+}