[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();
+ }
+}