blob: b6f27116f1dd1500db05c7b259999e01d5e07bbf [file] [log] [blame]
// 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:fuchsia_logger/logger.dart';
import 'package:fuchsia_services/services.dart' as app;
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 = 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);
}
@override
Future<void> registerMediaButtonsListener(
InterfaceHandle<MediaButtonsListener> listener) async {
await _model.presentation.registerMediaButtonsListener(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. May be null, in which case no metrics are logged.
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;
final List<KeyboardCaptureListenerHackBinding> _keyBindings = [];
final PresentationModeListenerBinding _presentationModeListenerBinding =
PresentationModeListenerBinding();
final PointerCaptureListenerHackBinding _pointerCaptureListenerBinding =
PointerCaptureListenerHackBinding();
final List<PresentationBinding> _presentationBindings =
<PresentationBinding>[];
CommonBaseShellPresentationImpl _presentationImpl;
/// Has the user logged in or not yet?
bool _loggedIn = false;
/// Constructor
CommonBaseShellModel([this.logger]) : super() {
_presentationImpl = CommonBaseShellPresentationImpl(this);
}
List<Account> get accounts => _accounts;
// |ServiceProvider|.
@override
Future<void> connectToService(String serviceName, Channel channel) {
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 (_loggedIn) {
log.warning(
'Ignoring unsupported attempt to log in while already logged in!',
);
return;
}
Timeline.instantSync('logging in', arguments: {'accountId': '$accountId'});
if (logger != null) {
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. ',
);
}
});
}
_userManager.login(accountId);
_loggedIn = true;
notifyListeners();
}
/// Called when the the session shell logs out.
@mustCallSuper
Future<void> onLogout() async {
_loggedIn = false;
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.StartupContext.fromStartupInfo()
.incoming
.connectToService(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 {
if (logger != null) {
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;
}