blob: 3d76d7106215f9386acdb5734f1809ea20e03123 [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:convert';
import 'dart:developer' show Timeline;
import 'dart:typed_data';
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_mem/fidl_async.dart' as fuchsia_mem;
import 'package:fidl_fuchsia_modular/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewHolderToken;
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_modular/lifecycle.dart' as lifecycle;
import 'package:fuchsia_services/services.dart';
import 'package:lib.story_shell/common.dart';
import 'package:zircon/zircon.dart';
import 'models/surface/surface_graph.dart';
import 'models/surface/surface_properties.dart';
import 'visual_state_watcher.dart';
/// An implementation of the [StoryShell] interface.
class StoryShellImpl extends StoryShell {
final StoryShellBinding _storyShellBinding = StoryShellBinding();
final StoryShellContextProxy _storyShellContext = StoryShellContextProxy();
final LinkProxy _linkProxy = LinkProxy();
final KeyListener keyListener;
final StoryVisualStateWatcherBinding _visualStateWatcherBinding =
StoryVisualStateWatcherBinding();
final SurfaceGraph surfaceGraph;
final String _storyShellLinkName = 'story_shell_state';
final StreamController<String> _focusEventStreamController =
StreamController.broadcast();
String _lastFocusedSurfaceId;
lifecycle.Lifecycle _lifecycle;
VisualStateWatcher _storyVisualStateWatcher;
StoryShellImpl({this.surfaceGraph, this.keyListener});
/// StoryShell
@override
Future<void> initialize(
InterfaceHandle<StoryShellContext> contextHandle) async {
_storyVisualStateWatcher = VisualStateWatcher(
keyListener: keyListener, storyShellContext: _storyShellContext);
_storyShellContext.ctrl.bind(contextHandle);
await _storyShellContext.watchVisualState(
_visualStateWatcherBinding.wrap(_storyVisualStateWatcher));
await _storyShellContext
.getLink(_linkProxy.ctrl.request())
.then((v) => reloadStoryState())
.then((onLinkContentsFetched));
surfaceGraph.addListener(() {
String surfaceId = surfaceGraph.focused?.node?.value;
if (surfaceId != null && surfaceId != _lastFocusedSurfaceId) {
_focusEventStreamController.add(surfaceId);
_lastFocusedSurfaceId = surfaceId;
}
});
}
/// Bind an [InterfaceRequest] for a [StoryShell] interface to this object.
void bindStoryShell(InterfaceRequest<StoryShell> request) {
_storyShellBinding.bind(this, request);
}
/// Introduce a new Surface and corresponding [ViewHolderToken] to the current
/// Story.
///
/// The Surface may have a relationship with its parent surface.
@override
Future<void> addSurface(
ViewConnection viewConnection,
SurfaceInfo surfaceInfo,
) async {
Timeline.instantSync(
'connecting surface ${viewConnection.surfaceId} with parent ${surfaceInfo.parentId}');
log.fine(
'Connecting surface ${viewConnection.surfaceId} with parent ${surfaceInfo.parentId}');
/// ignore: cascade_invocations
log.fine('Were passed manifest: $surfaceInfo.moduleManifest');
surfaceGraph
..addSurface(
viewConnection.surfaceId,
SurfaceProperties(source: surfaceInfo.moduleSource),
surfaceInfo.parentId,
surfaceInfo.surfaceRelation ?? const SurfaceRelation(),
surfaceInfo.moduleManifest != null
? surfaceInfo.moduleManifest.compositionPattern
: '',
surfaceInfo.moduleManifest != null
? surfaceInfo.moduleManifest.placeholderColor
: '',
)
..connectViewFromViewHolderToken(
viewConnection.surfaceId,
ViewHolderToken(
value:
EventPair(viewConnection.owner.passChannel().passHandle())));
}
/// Focus the surface with this id
@override
Future<void> focusSurface(String surfaceId) async {
Timeline.instantSync('focusing view',
arguments: {'surfaceId': '$surfaceId'});
surfaceGraph.focusSurface(surfaceId);
return persistStoryState();
}
/// Defocus the surface with this id
@override
Future<void> defocusSurface(String surfaceId) async {
Timeline.instantSync('defocusing view',
arguments: {'surfaceId': '$surfaceId'});
surfaceGraph.dismissSurface(surfaceId);
return persistStoryState();
}
@Deprecated('Deprecated')
@override
Future<void> addContainer(
String containerName,
String parentId,
SurfaceRelation relation,
List<ContainerLayout> layouts,
List<ContainerRelationEntry> relationships,
List<ContainerView> views,
) async {
// Add a root node for the container
Timeline.instantSync('adding container', arguments: {
'containerName': '$containerName',
'parentId': '$parentId'
});
surfaceGraph.addContainer(
containerName,
SurfaceProperties(),
parentId,
relation,
layouts,
);
Map<String, ContainerRelationEntry> nodeMap =
<String, ContainerRelationEntry>{};
Map<String, List<String>> parentChildrenMap = <String, List<String>>{};
Map<String, ViewHolderToken> viewMap = <String, ViewHolderToken>{};
for (ContainerView view in views) {
viewMap[view.nodeName] = ViewHolderToken(
value: EventPair(view.owner.passChannel().passHandle()));
}
for (ContainerRelationEntry relatedNode in relationships) {
nodeMap[relatedNode.nodeName] = relatedNode;
parentChildrenMap
.putIfAbsent(relatedNode.parentNodeName, () => <String>[])
.add(relatedNode.nodeName);
}
List<String> nodeQueue =
views.map((ContainerView v) => v.nodeName).toList();
List<String> addedParents = <String>[containerName];
int i = 0;
while (nodeQueue.isNotEmpty) {
String nodeId = nodeQueue.elementAt(i);
String parentId = nodeMap[nodeId].parentNodeName;
if (addedParents.contains(parentId)) {
for (nodeId in parentChildrenMap[parentId]) {
SurfaceProperties prop = SurfaceProperties()
..containerMembership = <String>[containerName]
..containerLabel = nodeId;
surfaceGraph.addSurface(
nodeId, prop, parentId, nodeMap[nodeId].relationship, null, '');
addedParents.add(nodeId);
surfaceGraph.connectViewFromViewHolderToken(nodeId, viewMap[nodeId]);
nodeQueue.remove(nodeId);
surfaceGraph.focusSurface(nodeId);
}
i = 0;
} else {
i++;
if (i > nodeQueue.length) {
log.warning('''Error iterating through container children.
All nodes iterated without finding all parents specified in
Container Relations''');
return;
}
}
}
}
@override
Future<void> removeSurface(String surfaceId) async {
return surfaceGraph.removeSurface(surfaceId);
}
@override
Future<void> reconnectView(ViewConnection viewConnection) async {
// TODO (jphsiao): implement
}
@override
Future<void> updateSurface(
ViewConnection viewConnection,
SurfaceInfo surfaceInfo,
) async {
// TODO (jphsiao): implement
}
Future<fuchsia_mem.Buffer> reloadStoryState() async {
return _linkProxy.get([_storyShellLinkName]);
}
void onLinkContentsFetched(fuchsia_mem.Buffer buffer) {
var dataVmo = SizedVmo(buffer.vmo.handle, buffer.size);
var data = dataVmo.read(buffer.size);
dataVmo.close();
dynamic decoded = jsonDecode(utf8.decode(data.bytesAsUint8List()));
if (decoded is Map<String, dynamic>) {
surfaceGraph.reload(decoded.cast<String, dynamic>());
}
}
Future<void> persistStoryState() async {
String encoded = json.encode(surfaceGraph);
var jsonList = Uint8List.fromList(utf8.encode(encoded));
var data = fuchsia_mem.Buffer(
vmo: SizedVmo.fromUint8List(jsonList),
size: jsonList.length,
);
await _linkProxy.set([_storyShellLinkName], data);
}
@override
Stream<String> get onSurfaceFocused => _focusEventStreamController.stream;
/// Start advertising the StoryShell service, and bind lifecycle to
/// termination
void advertise() {
StartupContext.fromStartupInfo().outgoing.addPublicService(
(InterfaceRequest<StoryShell> request) {
Timeline.instantSync('story shell request');
log.fine('Received binding request for StoryShell');
bindStoryShell(request);
},
StoryShell.$serviceName,
);
_lifecycle ??= lifecycle.Lifecycle();
}
}