blob: 5392903067e0fb31da336729b9303cb146568165 [file] [log] [blame]
// Copyright 2019 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 'package:meta/meta.dart';
import 'package:fidl/fidl.dart' show InterfaceRequest;
import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
import 'package:fidl_fuchsia_ui_policy/fidl_async.dart'
show Presentation, PresentationProxy;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewHolderToken;
import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
show ChildViewConnection;
import 'package:fuchsia_services/services.dart' show StartupContext;
import '../../session_shell.dart';
import '../../story.dart';
import '_focus_request_watcher_impl.dart';
import '_focus_watcher_impl.dart';
import '_modular_session_shell_impl.dart';
import '_session_shell_presentation_provider_impl.dart';
import '_story_provider_watcher_impl.dart';
typedef StoryFactory = Story Function({
SessionShell sessionShell,
modular.StoryInfo info,
modular.StoryController controller,
});
typedef StoryCallback = void Function(Story);
/// Defines a class that encapsulates FIDL interfaces used to build a 'Session
/// Shell' for Fuchsia.
///
/// A Session Shell's primary responsibility is to display
/// and manage a set of [Story] instances. As such it provides a Session Shell
/// author a set of callbacks to be notified when a story is started, stopped
/// or changed. It allows stories to be deleted and focused.
class SessionShellImpl implements SessionShell {
/// The [StartupContext] used to initialize SessionShell.
final StartupContext startupContext;
/// Callback when a new [Story] is started. Returns an instance of [Story].
final StoryFactory onStoryStarted;
/// Callback when a [Story] is deleted.
final StoryCallback onStoryDeleted;
/// Callback when a [Story] is changed.
final StoryCallback onStoryChanged;
/// Holds the [Story] instance mapped by it's id.
final _stories = <String, Story>{};
final _focusController = modular.FocusControllerProxy();
final _focusProvider = modular.FocusProviderProxy();
final _focusRequestWatcherBinding = modular.FocusRequestWatcherBinding();
final _focusWatcherBinding = modular.FocusWatcherBinding();
final _sessionShellBinding = modular.SessionShellBinding();
final _storyProviderWatcherBinding = modular.StoryProviderWatcherBinding();
final _presentationBindings =
<modular.SessionShellPresentationProviderBinding>[];
final _visualStateWatchers = <String, modular.StoryVisualStateWatcherProxy>{};
modular.SessionShellContextProxy _sessionShellContext;
modular.PuppetMasterProxy _puppetMaster;
modular.StoryProviderProxy _storyProvider;
PresentationProxy _presentation;
/// Constructor.
SessionShellImpl({
@required this.startupContext,
@required this.onStoryStarted,
this.onStoryChanged,
this.onStoryDeleted,
}) : assert(startupContext != null),
assert(onStoryStarted != null);
/// Register this instance of Session Shell with modular framework.
@override
void start() {
ArgumentError.checkNotNull(startupContext, 'startupContext');
// Watch modular framework for stories and focus.
watch(storyProvider, context);
// Advertise [modular.SessionShell] service.
startupContext.outgoing.addPublicService(
(InterfaceRequest<modular.SessionShell> request) => _sessionShellBinding
.bind(ModularSessionShellImpl(_attachView, _detachView), request),
modular.SessionShell.$serviceName);
// Advertise [modular.SessionShellPresentationProvider] service.
startupContext.outgoing.addPublicService(
(InterfaceRequest<modular.SessionShellPresentationProvider> request) =>
_presentationBindings.add(
modular.SessionShellPresentationProviderBinding()
..bind(
SessionShellPresentationProviderImpl(context.getPresentation,
(storyId, watcher) {
_visualStateWatchers[storyId] =
modular.StoryVisualStateWatcherProxy()
..ctrl.bind(watcher);
_updateVisualStateWatchers();
}),
request),
),
modular.SessionShellPresentationProvider.$serviceName,
);
}
/// Unregister and disconnect from modular framework.
@override
void stop() {
_focusProvider.ctrl.close();
_focusWatcherBinding.close();
_focusRequestWatcherBinding.close();
_focusController.ctrl.close();
_storyProviderWatcherBinding.close();
_storyProvider.ctrl.close();
_sessionShellContext.ctrl.close();
}
/// The list of stories in the system.
@override
Iterable<Story> get stories => _stories.values;
/// The [Story] that is currently focused. It can be null if no story is
/// currently in focus.
@override
Story focusedStory;
/// Request focus for story with [id].
@override
void focusStory(String id) => _onFocusRequest(id);
/// Delete the story with the id.
@override
void deleteStory(String id) {
puppetMaster.deleteStory(id);
_onDelete(id);
}
/// Stop the story with the id.
@override
void stopStory(String id) {
_onStopRequest(id);
}
/// Returns the [SessionShellContext].
@override
modular.SessionShellContext get context {
if (_sessionShellContext == null) {
_sessionShellContext = modular.SessionShellContextProxy();
startupContext.incoming.connectToService(_sessionShellContext);
}
return _sessionShellContext;
}
/// Returns the [Presentation] proxy.
@override
Presentation get presentation {
if (_presentation == null) {
_presentation = PresentationProxy();
context.getPresentation(_presentation.ctrl.request());
}
return _presentation;
}
/// Returns the [StoryProvider] interface from [SessionShellContext].
@visibleForTesting
modular.StoryProvider get storyProvider {
if (_storyProvider == null) {
_storyProvider = modular.StoryProviderProxy();
context.getStoryProvider(_storyProvider.ctrl.request());
}
return _storyProvider;
}
/// Returns the [PuppetMaster] interface from [StartupContext].
@override
modular.PuppetMaster get puppetMaster {
if (_puppetMaster == null) {
_puppetMaster = modular.PuppetMasterProxy();
startupContext.incoming.connectToService(_puppetMaster);
}
return _puppetMaster;
}
/// Watch modular framework for stories and focus.
@visibleForTesting
void watch(
modular.StoryProvider storyProvider,
modular.SessionShellContext context,
) {
ArgumentError.checkNotNull(storyProvider, 'storyProvider');
ArgumentError.checkNotNull(context, 'context');
storyProvider.watch(_storyProviderWatcherBinding
.wrap(StoryProviderWatcherImpl(onChange, _onDelete)));
context.getFocusController(_focusController.ctrl.request());
_focusController.watchRequest(_focusRequestWatcherBinding
.wrap(FocusRequestWatcherImpl(_onFocusRequest)));
context.getFocusProvider(_focusProvider.ctrl.request());
_focusProvider
.watch(_focusWatcherBinding.wrap(FocusWatcherImpl(onFocusChange)));
}
/// Called by [modular.StoryProviderWatcher] to update story state.
@visibleForTesting
void onChange(
modular.StoryInfo info,
modular.StoryState state,
modular.StoryVisibilityState visibilityState,
) {
if (!_stories.containsKey(info.id)) {
if (state == modular.StoryState.stopped) {
final storyController = newStoryController();
storyProvider.getController(info.id, storyController.ctrl.request());
storyController.requestStart();
_stories[info.id] = onStoryStarted(
info: info,
sessionShell: this,
controller: storyController,
)
..state = state
..visibilityState = visibilityState;
onStoryChanged?.call(_stories[info.id]);
}
} else {
_stories[info.id]
..state = state
..visibilityState = visibilityState;
onStoryChanged?.call(_stories[info.id]);
}
}
/// Returns a new instance of [modular.StoryControllerProxy].
@visibleForTesting
modular.StoryControllerProxy newStoryController() =>
modular.StoryControllerProxy();
/// Called by [StoryProviderWatcherImpl].
void _onDelete(String storyId) {
if (!_stories.containsKey(storyId)) {
return;
}
final story = _stories[storyId];
_stories.remove(storyId);
_visualStateWatchers.remove(storyId);
if (focusedStory == story) {
focusedStory = null;
}
onStoryDeleted?.call(story);
}
/// Request focus from [modular.FocusController].
void _onFocusRequest(String storyId) {
_focusController.set(storyId);
}
void _onStopRequest(String storyId) async {
if (_stories.containsKey(storyId)) {
final storyController = newStoryController();
await storyProvider.getController(
storyId, storyController.ctrl.request());
await storyController.stop();
}
}
/// Called by [modular.FocusWatcher] to change focus on a story.
@visibleForTesting
void onFocusChange(modular.FocusInfo focusInfo) {
if (focusedStory != null) {
if (focusInfo.focusedStoryId == focusedStory.id) {
return;
}
focusedStory.focused = false;
onStoryChanged?.call(focusedStory);
}
assert(_stories.containsKey(focusInfo.focusedStoryId));
focusedStory = _stories[focusInfo.focusedStoryId]..focused = true;
onStoryChanged?.call(focusedStory);
_updateVisualStateWatchers();
}
/// Called from [ModularSessionShellImpl].
void _attachView(
modular.ViewIdentifier viewId, ViewHolderToken viewHolderToken) {
_stories[viewId.storyId]?.childViewConnection =
ChildViewConnection(viewHolderToken);
onStoryChanged?.call(_stories[viewId.storyId]);
}
/// Called from [ModularSessionShellImpl].
void _detachView(modular.ViewIdentifier viewId) {
_stories[viewId.storyId]?.childViewConnection = null;
onStoryChanged?.call(_stories[viewId.storyId]);
}
void _updateVisualStateWatchers() {
for (final storyId in _visualStateWatchers.keys) {
_visualStateWatchers[storyId].onVisualStateChange(
storyId == focusedStory?.id
? modular.StoryVisualState.maximized
: modular.StoryVisualState.minimized);
}
}
}