| // 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); |
| } |
| } |