blob: b72e0b1f798f1900cdbd286859d9a335f121f0d8 [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 'dart:convert';
import 'dart:typed_data';
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_mem/fidl.dart' as fuchsia_mem;
import 'package:fidl_fuchsia_sys/fidl.dart';
import 'package:lib.app.dart/app.dart';
import 'package:lib.app.dart/logging.dart';
import 'package:lib.app_driver.dart/intent_parameters.dart';
import 'package:lib.component.dart/component.dart';
import 'package:lib.entity.dart/entity.dart';
import 'package:lib.intent_handler.dart/intent_handler.dart';
import 'package:lib.lifecycle.dart/lifecycle.dart';
import 'package:lib.module.dart/module.dart';
import 'package:lib.schemas.dart/entity_codec.dart';
import 'package:lib.story.dart/story.dart';
import 'package:meta/meta.dart';
import 'package:zircon/zircon.dart';
import 'service_client.dart';
export 'package:lib.component.dart/component.dart'
show MessageQueueError, MessageSenderError;
export 'package:fidl_fuchsia_modular/fidl.dart' show Intent;
export 'package:lib.app_driver.dart/intent_parameters.dart'
show IntentParameters;
export 'package:lib.module_resolver.dart/intent_builder.dart'
show IntentBuilder;
export 'package:lib.module.dart/module.dart'
show ModuleControllerClient, EmbeddedModule;
export 'package:lib.story.dart/story.dart' show LinkClient;
/// Function definition to handle [data] that is received from a message queue.
typedef OnReceiveMessage = void Function(Uint8List data, void Function() ack);
/// Function to run when the module terminates
typedef OnTerminate = void Function();
/// Function to run when the module terminates asynchronously
typedef OnTerminateAsync = Future<Null> Function();
typedef OnHandleIntent = void Function(
String action, IntentParameters parameters);
/// The [ModuleDriver] provides a high-level API for running a module in Dart
/// code. The name and structure of this library is based on the peridot layer's
/// [AppDriver][app-driver]. A Module has two primary events:
///
/// * initialize: managed by the constructor.
/// * terminate: managed by the internal [LifecycleHost].
///
/// Initialization
///
/// Module initialization is triggered by calling [startSync]. Once the module
/// has successfully initialized additional service clients are connected
/// providing access to The Module's Link and ModuleContext services.
///
/// Termination
///
/// Module termination is triggered by the system, all service hosts and clients
/// will automatically have their underlying connections closed including any
/// added by making calls to exposed APIs (e.g. [link], [moduleContext]).
@Deprecated('Use package:fuchsia_modular/module.dart instead')
class ModuleDriver {
final StartupContext _startupContext = new StartupContext.fromStartupInfo();
/// A [LinkClient] for this module's default Link. Async results for
/// LinkClient methods will resolve once the Module has been initialized
/// successfully. If access to more links is required use
/// [moduleContext#getLink()].
final LinkClient link = new LinkClient();
/// Called when a new intent is received from the framework. When set the
/// module driver will call [onHandleIntent] with the latest intent
/// received, if such an intent is available.
OnHandleIntent _onHandleIntent;
/// Stores the latest intent received from the framework so it can be passed to
/// [onHandleIntent] when it is set.
Intent _intent;
/// The [ModuleContextClient] for this module. Async results for method calls
/// will resolve once the Module has been initialized successfully.
final ModuleContextClient moduleContext = new ModuleContextClient();
// Message queue proxies for receiving updates from the content provider
// TODO(meiyili): update to handle creating to multiple message queues MS-1288
MessageQueueClient _messageQueue;
final Set<String> _firstObservationSent = <String>{};
LifecycleHost _lifecycle;
IntentHandlerImpl _intentHandler;
/// Shadow async completion of [startSync].
final _start = Completer<ModuleDriver>();
/// A flag indicating if the user has called [startSync] yet.
var _hasCalledStart = false;
// Methods to run when the module is being torn down
final List<OnTerminateAsync> _onTerminatesAsync = <OnTerminateAsync>[];
/// Deprecated: Use package:fuchsia_modular/module.dart instead
///
/// Create a new [ModuleDriver].
///
/// ModuleDriver module = new ModuleDriver();
///
/// [onTerminate] lets the caller specify a function to run in the
/// onTerminate call.
///
/// Register for link updates:
///
/// module.link.watch()
/// .listen((Object json) => print('Link data: $json'));
///
/// Start the module:
///
/// module.startSync();
///
ModuleDriver({
// TODO(MS-1521): consider removing
OnTerminate onTerminate,
}) {
if (onTerminate != null) {
_onTerminatesAsync.add(() async {
onTerminate();
return null;
});
}
_lifecycle = new LifecycleHost(
onTerminate: _handleTerminate,
);
_intentHandler = IntentHandlerImpl(onHandleIntent: _handleIntent);
// Observe time to default link
// TODO(meiyili): remove once default link is deprecated
link.watch().listen((String data) => _observeLinkData('default', data));
}
/// Sets the intent handler.
set onHandleIntent(OnHandleIntent onHandleIntent) {
_onHandleIntent = onHandleIntent;
_handleIntent(_intent);
}
String _moduleName;
/// Module name to use for metrics, if it is not set will use default package
/// name for the module
String get moduleName => _moduleName;
set moduleName(String name) => _moduleName = name.trim();
/// Add other methods to run on terminate
void addOnTerminateHandler(OnTerminate onTerminate) => _onTerminatesAsync.add(
() async {
onTerminate();
return null;
},
);
/// Add a method to run on terminate asynchronously
void addOnTerminateAsyncHandler(OnTerminateAsync onTerminate) =>
_onTerminatesAsync.add(onTerminate);
/// Deprecated use [#startSync] instead.
///
/// Start the module and connect to dependent services on module
/// initialization
// @Deprecated('use startSync instead') can't add this because of linter
Future<ModuleDriver> start() async {
log.fine('#start(...)');
startSync();
return _start.future;
}
/// Start the module and connect to dependent services on module
/// initialization.
void startSync() {
log.fine('#startSync()');
// Fail fast on subsequent (accidental) calls to #start() instead of
// triggering deeper errors by re-binding the impl.
if (_hasCalledStart) {
throw Exception('moduleDrive.startSync()) should only be called once.');
}
_hasCalledStart = true;
_lifecycle.addService(startupContext: _startupContext);
_intentHandler.addService(startupContext: _startupContext);
connectToService(environmentServices, moduleContext.proxy.ctrl);
moduleContext.getLink(linkClient: link);
// Still complete the future since the rest of the login in this class
// awaiting on it.
_start.complete(this);
}
/// Creates a message queue and returns a [Future] with the message queue
/// token that should be passed to agents we want to connect to. If a
/// message queue has already been created, it will return the token for the
/// token for the already created queue and ignore the new [onMessage] method.
///
/// [name] is the name of the message queue.
/// [onMessage] should be supplied to handle the data from the message queue.
// TODO(meiyili): Update to allow creating multiple message queues MS-1288
Future<String> createMessageQueue({
@required String name,
@required OnReceiveMessage onMessage,
@required MessageQueueErrorCallback onConnectionError,
}) async {
assert(name != null && name.isNotEmpty);
assert(onMessage != null);
log.fine('#createMessageQueue($name)');
// Create a message queue that the module can pass to agents only if we
// haven't already created one
ComponentContextClient componentContext =
await moduleContext.getComponentContext();
_messageQueue = componentContext.obtainMessageQueue(
name: name, onMessage: onMessage, onConnectionError: onConnectionError);
return _messageQueue.getToken();
}
/// Connect to the service specified by [client] and implemented by the
/// agent at [url].
///
/// The [url] is required as multiple agents can implement the same
/// service interface.
Future<void> connectToAgentService<T>(
String url,
ServiceClient<T> client,
) async {
await _start.future.timeout(Duration(seconds: 5));
await connectToAgentServiceWithProxy(url, client.proxy);
}
/// Connect to the service specified by [proxy] and implemented by the
/// agent at [url].
/// DEPRECATED: please write a client for your service and use
/// connectToAgentService
Future<void> connectToAgentServiceWithProxy(
String url,
Proxy<dynamic> proxy,
) async {
log.fine('#connectToAgentService(...)');
assert(proxy.ctrl.$serviceName != null,
'controller.\$serviceName must not be null. Check the FIDL file for a missing [Discoverable]');
ComponentContextClient componentContext = await getComponentContext();
ServiceProviderProxy serviceProviderProxy =
await componentContext.connectToAgent(url);
connectToService(serviceProviderProxy, proxy.ctrl);
// Close all unnecessary bindings
serviceProviderProxy.ctrl.close();
}
/// Connect to an agent using a new-style async proxy.
Future<void> connectToAgentServiceWithAsyncProxy(
String url,
AsyncProxy<dynamic> proxy,
) async {
log.fine('#connectToAgentService(...)');
assert(proxy.ctrl.$serviceName != null,
'controller.\$serviceName must not be null. Check the FIDL file for a missing [Discoverable]');
final serviceName = proxy.ctrl.$serviceName;
ComponentContextClient componentContext = await getComponentContext();
await componentContext.connectToAgent(url)
..connectToService(serviceName, proxy.ctrl.request().passChannel())
// Close all unnecessary bindings
..ctrl.close();
}
/// Retrieve the story id of the story this module lives in
Future<String> getStoryId() {
log.fine('#getStoryId(...)');
return moduleContext.getStoryId();
}
Future<Null> _handleTerminate() {
log.info('closing service connections');
_messageQueue?.close();
List<Future<Null>> futures = <Future<Null>>[
moduleContext.terminate(),
_lifecycle.terminate(),
]..addAll(
_onTerminatesAsync.map((OnTerminateAsync onTerminate) => onTerminate()),
);
return Future.wait(futures).then((_) {
log.info('successfully closed all service connections');
}, onError: (Error err, StackTrace stackTrace) {
log.warning('failed to close all service connections');
throw err;
});
}
/// Save when the link data first becomes non-null.
void _observeLinkData(String linkName, String data) {
if (!_firstObservationSent.contains(linkName) && data != null) {
_firstObservationSent.add(linkName);
}
}
void _handleIntent(Intent intent) {
log.info('Received intent from framework');
_intent = intent;
if (_onHandleIntent != null && intent != null) {
_onHandleIntent(intent.action,
IntentParameters(moduleDriver: this, parameters: intent.parameters));
}
}
/// Watch for Entity updates from Link with the name [key] and automatically
/// decode values using [codec].
Stream<T> watch<T>(String key, EntityCodec<T> codec, {bool all = false}) {
StreamController<String> controller = new StreamController<String>(
onListen: () => log.info('watch stream ($key): listening'),
onPause: () => log.info('watch stream ($key): paused'),
onResume: () => log.info('watch stream ($key): resuming'),
onCancel: () => log.info('watch stream ($key): cancelled'),
);
Future<String> convert(_) async {
// The String data will be null but signals that Link#setEntity(..) was
// called and a new value should be retrieved.
LinkClient link = await getLink(key);
log.fine('getting Entity ref for "$key" via $link');
// QUESTION: can the ref value change between updates to the same entity
// values set by ComponentContext#createEntityWithData(...)?
String ref = await link.getEntity();
// If the link does not contain an entity, attempt to get it as json
// before returning an error.
if (ref == null) {
fuchsia_mem.Buffer buffer = await link.get();
if (buffer == null) {
return null;
}
var dataVmo = new SizedVmo(buffer.vmo.handle, buffer.size);
var data = dataVmo.read(buffer.size);
dataVmo.close();
return jsonDecode(utf8.decode(data.bytesAsUint8List()));
}
EntityResolverClient resolver = await getResolver();
EntityClient entity = await resolver.resolveEntity(ref);
List<String> types = await entity.getTypes();
if (!types.contains(codec.type)) {
// This exception will be caught and the pending future will be resolved
// with this error.
throw new EntityTypeException(codec.type);
}
return await entity.getData(codec.type);
}
// NOTE: do not use await, the controller.stream needs to be returned
// synchronously so listeners can be attached without extra async book
// keeping.
getLink(key).then((LinkClient link) {
log.info('watching link "${link.name}" for Entity updates');
Stream<String> source = link.watch(all: all).asyncMap(convert);
controller.addStream(source, cancelOnError: true).then((_) {
log.info('link stream is "done"');
controller.close();
});
}, onError: controller.addError).catchError(controller.addError);
Stream<T> stream = controller.stream.transform(codec.decoder);
return stream;
}
/// Watch for Entity updates from Link with the name [key] and return the
/// raw bytes in the Link's value.
/// decode values using [codec].
Stream<Uint8List> watchRaw(String key, {bool all = false}) {
StreamController<Uint8List> controller = new StreamController<Uint8List>(
onListen: () => log.info('watchRaw stream ($key): listening'),
onPause: () => log.info('watchRaw stream ($key): paused'),
onResume: () => log.info('watchRaw stream ($key): resuming'),
onCancel: () => log.info('watchRaw stream ($key): cancelled'),
);
Future<Uint8List> convert(_) async {
fuchsia_mem.Buffer buffer = await link.get();
if (buffer == null) {
return null;
}
var dataVmo = new SizedVmo(buffer.vmo.handle, buffer.size);
var data = dataVmo.read(buffer.size);
dataVmo.close();
return data.bytesAsUint8List();
}
// NOTE: do not use await, the controller.stream needs to be returned
// synchronously so listeners can be attached without extra async book
// keeping.
getLink(key).then((LinkClient link) {
log.info('watching link "${link.name}" for Entity updates');
Stream<Uint8List> source = link.watch(all: all).asyncMap(convert);
controller.addStream(source, cancelOnError: true).then((_) {
log.info('link stream is "done"');
controller.close();
});
}, onError: controller.addError).catchError(controller.addError);
return controller.stream;
}
/// Create or update the Entity's value (translated per [codec]) and persist
/// it to a Link with name [key].
Future<String> put<T>(String key, T value, EntityCodec<T> codec) async {
Completer<String> completer = new Completer<String>();
try {
ComponentContextClient componentContext = await getComponentContext();
String ref =
await componentContext.createEntityWithData(<TypeToDataEntry>[
new TypeToDataEntry(type: codec.type, data: codec.encode(value)),
]);
LinkClient link = await getLink(key);
await link.setEntity(ref);
completer.complete(ref);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
/// Retrieve the current value stored on the Link with the name [key].
/// The value is converted from an Entity and decoded using [codec].
Future<T> get<T>(String key, EntityCodec<T> codec) async {
Completer<T> completer = new Completer<T>();
try {
LinkClient link = await getLink(key);
String entityRef = await link.getEntity();
if (entityRef == null) {
return null;
}
EntityResolverClient resolver = await getResolver();
EntityClient entity = await resolver.resolveEntity(entityRef);
List<String> types = await entity.getTypes();
if (!types.contains(codec.type)) {
throw new EntityTypeException(codec.type);
}
String data = await entity.getData(codec.type);
completer.complete(codec.decoder.convert(data));
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
return completer.future;
}
final Map<String, LinkClient> _links = <String, LinkClient>{};
/// Async access to the underlying [LinkClient] with the name [name].
Future<LinkClient> getLink(String name) async {
assert(name != null);
assert(name.isNotEmpty);
Completer<LinkClient> completer = new Completer<LinkClient>();
if (_links.containsKey(name)) {
completer.complete(_links[name]);
} else {
try {
LinkClient client = _links[name] = new LinkClient(name: name);
await moduleContext.getLink(linkClient: client);
client.watch().listen((String data) => _observeLinkData(name, data));
completer.complete(client);
} on Exception catch (err, stackTrace) {
completer.completeError(err, stackTrace);
}
}
return completer.future;
}
/// Report that this module is active even without user interaction. This
/// is used by the story shell to allow long running modules to avoid
/// timeout due to user inactivity.
void active() {
log.fine('#active(...)');
moduleContext.active();
}
/// When a module calls [done] the framework will stop the module. If there
/// are no more running modules in the story the story will be stopped.
Future<void> done() {
return moduleContext.done();
}
/// Cache for [getComponentContext].
Completer<ComponentContextClient> _componentContext;
/// Async access to the [ComponentContextClient].
Future<ComponentContextClient> getComponentContext() async {
if (_componentContext == null) {
_componentContext = new Completer<ComponentContextClient>();
try {
_componentContext.complete(await moduleContext.getComponentContext());
} on Exception catch (err, stackTrace) {
_componentContext.completeError(err, stackTrace);
}
}
return _componentContext.future;
}
/// Shadow cache for [getResolver].
Completer<EntityResolverClient> _resolver;
/// Async access to the [EntityResolverClient].
Future<EntityResolverClient> getResolver() async {
if (_resolver == null) {
_resolver = new Completer<EntityResolverClient>();
try {
ComponentContextClient context = await getComponentContext();
_resolver.complete(await context.getEntityResolver());
} on Exception catch (err, stackTrace) {
_resolver.completeError(err, stackTrace);
}
}
return _resolver.future;
}
/// # Start Module
///
/// Start a module and display it based on the passed in [surfaceRelation].
///
/// On successful resolution the Future completes with a
/// [ModuleControllerClient] that allows the parent module (the one calling
/// this method) to access methods for observing and controlling the Module.
///
/// Related FIDL APIs:
///
/// * [ModuleContext#StartModule](https://goo.gl/9T8Gkv).
/// * [ModuleController](https://goo.gl/ZXcYW3).
///
/// TODO(MS-1714): collapse name params or clearly document their differences.
Future<ModuleControllerClient> startModule({
@required Intent intent,
String name,
String module,
SurfaceRelation surfaceRelation = const SurfaceRelation(
arrangement: SurfaceArrangement.copresent,
dependency: SurfaceDependency.dependent,
emphasis: 0.5,
),
}) async {
name ??= module;
assert(name != null && name.isNotEmpty);
assert(intent != null);
if (module != null) {
// TODO(MS-1443): remove deprecation notice once client usage has been
// updated.
log.warning('param "module" is deprecated, use "name" instead');
}
return moduleContext.addModuleToStory(
module: name,
intent: intent,
surfaceRelation: surfaceRelation,
);
}
/// # Embed Module
///
/// Calls underlying framework APIs to start and configure a module instance
/// for embedding within a Flutter Widget tree.
///
/// On successful resolution the Future completes with an [EmbeddedModule]
/// instance providing access to a [ChildView] Flutter Widget and a
/// [ModuleControllerClient].
///
/// Related FIDL APIs:
///
/// * [ModuleContext#EmbedModule](https://goo.gl/9T8Gkv).
/// * [ModuleController](https://goo.gl/ZXcYW3).
///
Future<EmbeddedModule> embedModule({
@required String name,
@required Intent intent,
}) async {
assert(name != null && name.isNotEmpty);
assert(intent != null);
log.fine('resolving module ("$name") for embedding...');
return moduleContext.embedModule(name: name, intent: intent);
}
/// Made available for video module to access MediaPlayer.
/// TODO(MS-1287): Determine whether this should be refactored
ServiceProviderProxy get environmentServices =>
_startupContext.environmentServices;
}
/// [app-driver]: https://fuchsia.googlesource.com/peridot/+/master/public/lib/app_driver/cpp?autodive=0/