blob: 1825743cbe7ee621f93440d97ad35fa1f9dc6cce [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: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;
/// 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;
ChildViewConnection get childViewConnectionNew => _childViewConnection;
set shouldCreateNewChildView(bool should) {}
@override
Future<void> captureKeyboardEventHack(input.KeyboardEvent eventToCapture,
InterfaceHandle<KeyboardCaptureListenerHack> listener) async {
await presentation.captureKeyboardEventHack(eventToCapture, listener);
}
@override
Future<void> capturePointerEventsHack(
InterfaceHandle<PointerCaptureListenerHack> listener) async {
await presentation.capturePointerEventsHack(listener);
}
// |ServiceProvider|.
@override
Future<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();
}
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();
}
}
@override
// ignore: avoid_positional_boolean_parameters
Future<void> enableClipping(bool enabled) async {
await presentation.enableClipping(enabled);
}
/// |Presentation|.
@override
Future<PresentationMode> getPresentationMode() async {
return await presentation.getPresentationMode();
}
/// 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();
}
/// |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;
}
}
/// |KeyboardCaptureListener|.
@override
Future<void> onEvent(input.KeyboardEvent ev) async {}
/// |PointerCaptureListener|.
@override
Future<void> onPointerEvent(input.PointerEvent event) async {}
// |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.connectToEnvironmentService(netstackProxy);
_netstackModel = NetstackModel(netstack: netstackProxy)..start();
await presentation
.capturePointerEventsHack(_pointerCaptureListenerBinding.wrap(this));
await presentation.setPresentationModeListener(
_presentationModeListenerBinding.wrap(this));
_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();
}
// |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
Future<void> setDisplayRotation(
double displayRotationDegrees, bool animate) async {
await presentation.setDisplayRotation(displayRotationDegrees, animate);
}
// |Presentation|.
@override
Future<void> setDisplaySizeInMm(num widthInMm, num heightInMm) async {
await presentation.setDisplaySizeInMm(widthInMm, heightInMm);
}
// |Presentation|.
@override
Future<void> setDisplayUsage(DisplayUsage usage) async {
await presentation.setDisplayUsage(usage);
}
// |Presentation|.
/// |Presentation|.
@override
Future<void> setPresentationModeListener(
InterfaceHandle<PresentationModeListener> listener) async {
await presentation.setPresentationModeListener(listener);
}
// |Presentation|.
@override
Future<void> setRendererParams(List<RendererParam> params) async {
await presentation.setRendererParams(params);
}
@override
Future<void> useOrthographicView() async {
await presentation.useOrthographicView();
}
@override
Future<void> usePerspectiveView() async {
await presentation.usePerspectiveView();
}
@override
// TODO: implement $serviceData
ServiceData get $serviceData => null;
}