blob: 849451b78e67f0e4ec69e581280e44d1e8ef9a19 [file] [log] [blame]
// Copyright 2018 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_modular/fidl.dart' as fidl;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewHolderToken;
import 'package:fidl_fuchsia_ui_viewsv1token/fidl.dart';
import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
show ChildViewConnection;
import 'package:lib.app.dart/logging.dart';
import 'package:lib.component.dart/component.dart';
import 'package:lib.story.dart/story.dart';
import 'package:meta/meta.dart';
import 'package:zircon/zircon.dart';
import 'module_controller_client.dart';
export 'package:lib.component.dart/component.dart' show ComponentContextClient;
/// When Module resolution fails.
class ResolutionException implements Exception {
/// Information about the failure.
final String message;
/// Create a new [ResolutionException].
ResolutionException(this.message);
}
/// Holds values necessary for interacting with Model and View related FIDL APIs
/// for modules started via [ModuleContextClient#embedModule].
class EmbeddedModule {
/// The client for the ModuleController FIDL service connected to an embedded
/// module.
final ModuleControllerClient controller;
/// The underlying ChildViewConnection, stored as a value here to prevent GC.
final ChildViewConnection connection;
/// The Flutter Widget that renders the UI of the started module. Do not
/// assume the view is ready to display pixels to this view, check the
/// controller to prevent jank.
final ChildView view;
/// Constructor, for usage see [ModuleContextClient#embedModule].
EmbeddedModule({
@required this.controller,
@required this.connection,
@required this.view,
}) : assert(controller != null),
assert(connection != null),
assert(view != null);
}
/// Client wrapper for [fidl.ModuleContext].
///
/// TODO(SO-1125): implement all methods for ModuleContextClient
class ModuleContextClient {
ComponentContextClient _componentContext;
final OngoingActivityProxy _ongoingActivityProxy = OngoingActivityProxy();
/// The underlying [Proxy] used to send client requests to the
/// [fidl.ModuleContext] service.
final fidl.ModuleContextProxy proxy = fidl.ModuleContextProxy();
final List<LinkClient> _links = <LinkClient>[];
/// Constructor.
ModuleContextClient() {
proxy.ctrl
..onBind = _handleBind
..onClose = _handleClose
..onConnectionError = _handleConnectionError
..onUnbind = _handleUnbind;
}
final Completer<Null> _bind = Completer<Null>();
/// A future that completes when the [proxy] is bound.
Future<Null> get bound => _bind.future;
void _handleBind() {
log.fine('proxy ready');
_bind.complete(null);
}
/// Connects the passed in [LinkClient] via [fidl.ModuleContextProxy#getLink].
// TODO(MS-1245): retrun an active link client automatically instead of passing one
// through.
Future<Null> getLink({
@required LinkClient linkClient,
}) async {
log.fine('getLink: ${linkClient.name}');
// Track all the link clients so they can be closed automatically when this
// client is.
_links.add(linkClient);
Completer<Null> completer = Completer<Null>();
try {
await bound;
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
return completer.future;
}
InterfaceRequest<Link> request;
try {
// NOTE: Any async errors on the link's proxy should be managed by
// LinkClient.
request = linkClient.proxy.ctrl.request();
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
return completer.future;
}
try {
proxy.getLink(linkClient.name, request);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
scheduleMicrotask(() {
if (!completer.isCompleted) {
completer.complete(null);
}
});
return completer.future;
}
/// See [fidl.ComponentContext#getComponentContext].
Future<ComponentContextClient> getComponentContext() async {
await bound;
if (_componentContext != null) {
return _componentContext;
} else {
_componentContext = ComponentContextClient();
}
Completer<ComponentContextClient> completer =
Completer<ComponentContextClient>();
// ignore: unawaited_futures
proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
// ignore: unawaited_futures
_componentContext.proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
try {
proxy.getComponentContext(_componentContext.proxy.ctrl.request());
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
scheduleMicrotask(() {
if (!completer.isCompleted) {
completer.complete(_componentContext);
}
});
return completer.future;
}
/// See [fidl.ModuleContext#addModuleToStory].
Future<ModuleControllerClient> addModuleToStory({
@required String module,
@required Intent intent,
@required SurfaceRelation surfaceRelation,
}) async {
assert(module != null && module.isNotEmpty);
assert(intent != null);
assert(surfaceRelation != null);
Completer<ModuleControllerClient> completer =
Completer<ModuleControllerClient>();
// TODO(): map results and reuse for subsequent calls, see getLink.
ModuleControllerClient controller = ModuleControllerClient();
// ignore: unawaited_futures
proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
// ignore: unawaited_futures
controller.proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
void handleIntentStatus(fidl.StartModuleStatus status) {
switch (status) {
case fidl.StartModuleStatus.success:
completer.complete(controller);
break;
case fidl.StartModuleStatus.noModulesFound:
completer.completeError(ResolutionException('no modules found'));
break;
default:
completer.completeError(
ResolutionException('unknown status: $status'));
}
}
try {
proxy.addModuleToStory(
module,
intent,
controller.proxy.ctrl.request(),
surfaceRelation,
handleIntentStatus,
);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl.ModuleContext#embedModule].
Future<EmbeddedModule> embedModule({
@required String name,
@required Intent intent,
}) {
assert(name != null && name.isNotEmpty);
assert(intent != null);
Completer<EmbeddedModule> completer = Completer<EmbeddedModule>();
InterfacePair<ViewOwner> viewOwner = InterfacePair<ViewOwner>();
ModuleControllerClient controller = ModuleControllerClient();
void handleIntentStatus(fidl.StartModuleStatus status) {
log.fine('resolved "$name" with status "$status"');
switch (status) {
case fidl.StartModuleStatus.success:
log.fine('configuring view for "$name"');
// TODO(MS-1437): viewOwner error handling.
ChildViewConnection connection = ChildViewConnection(ViewHolderToken(
value: EventPair(viewOwner.passHandle().passChannel().passHandle()),
));
EmbeddedModule result = EmbeddedModule(
controller: controller,
connection: connection,
view: ChildView(connection: connection),
);
completer.complete(result);
break;
case fidl.StartModuleStatus.noModulesFound:
completer.completeError(ResolutionException('no modules found'));
break;
default:
completer.completeError(
ResolutionException('unknown status: $status'));
}
}
// ignore: unawaited_futures
proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
// ignore: unawaited_futures
controller.proxy.ctrl.error.then((ProxyError err) {
if (!completer.isCompleted) {
completer.completeError(err);
}
});
try {
proxy.embedModule(
name,
intent,
controller.proxy.ctrl.request(),
viewOwner.passRequest(),
handleIntentStatus,
);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl.ModuleContext#getStoryId].
Future<String> getStoryId() async {
Completer<String> completer = Completer<String>();
try {
await bound;
// ignore: unawaited_futures
proxy.ctrl.error.then((Object error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
});
proxy.getStoryId(completer.complete);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl:ModuleContext#active].
Future<Null> active() async {
Completer<Null> completer = Completer<Null>();
try {
await bound;
// ignore: unawaited_futures
proxy.ctrl.error.then((Object error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
});
proxy.active();
completer.complete(null);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl:ModuleContext#done]
Future<void> done() async {
Completer completer = Completer();
try {
await bound;
// ignore: unawaited_futures
proxy.ctrl.error.then((Object error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
});
proxy.removeSelfFromStory();
completer.complete();
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl:ModuleContext#RequestStoryVisibilityState]
Future<void> requestStoryVisibilityState(
fidl.StoryVisibilityState state) async {
log.fine('requestStoryVisibilityState requesting state $state');
Completer completer = Completer();
try {
await bound;
// ignore: unawaited_futures
proxy.ctrl.error.then((Object error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
});
proxy.requestStoryVisibilityState(state);
completer.complete();
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// See [fidl.ModuleContext#StartOngoingActivity].
Future<OngoingActivityProxy> startOngoingActivity(
OngoingActivityType type) async {
await bound;
Completer<OngoingActivityProxy> completer =
Completer<OngoingActivityProxy>();
try {
if (!_ongoingActivityProxy.ctrl.isBound) {
proxy.startOngoingActivity(type, _ongoingActivityProxy.ctrl.request());
}
completer.complete(_ongoingActivityProxy);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
void _handleConnectionError() {
log.severe('ModuleContextClient connection error');
}
void _handleClose() {
log.fine('proxy closed, terminating link clients');
}
void _handleUnbind() {
log.fine('proxy unbound');
}
/// Closes the underlying proxy connection, should be called as a response to
/// Lifecycle::terminate (see https://goo.gl/MmZ2dc).
Future<Null> terminate() async {
log.fine('terminate called');
proxy.ctrl.close();
return Future.wait(
_links.map((LinkClient link) => link.terminate()).toList())
.then((_) => null);
}
}