| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:fidl/fidl.dart'; |
| import 'package:fidl_fuchsia_cobalt/fidl.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:lib.widgets/modular.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:zircon/zircon.dart' show Channel; |
| |
| 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; |
| } |
| |
| trace('logging in $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(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(); |
| } |
| } |