[sdk] Add fuchsia_modular_flutter package.
This change adds fuchsia_modular_flutter package, containing a set
of classes for Session Shell. It is a Flutter package, hence _flutter
in its name.
This is used by Ermine session shell.
Test: Includes unittests for SessionShell and Story classes.
use `fx run-host-tests fuchsia_modular_flutter_unittests`
Change-Id: I0d678f8fbca860d430f3e9ff54fd30ecfce9cdf5
diff --git a/packages/tests/BUILD.gn b/packages/tests/BUILD.gn
index 1fb140d..337d944 100644
--- a/packages/tests/BUILD.gn
+++ b/packages/tests/BUILD.gn
@@ -25,6 +25,7 @@
"//topaz/public/dart/fuchsia_inspect:fuchsia_inspect_package_unittests($host_toolchain)",
"//topaz/public/dart/fuchsia_logger:fuchsia_logger_package_unittests($host_toolchain)",
"//topaz/public/dart/fuchsia_modular:fuchsia_modular_package_unittests($host_toolchain)",
+ "//topaz/public/dart/fuchsia_modular_flutter:fuchsia_modular_flutter_unittests($host_toolchain)",
"//topaz/public/dart/fuchsia_scenic_flutter:fuchsia_scenic_flutter_unittests($host_toolchain)",
"//topaz/public/dart/fuchsia_services:fuchsia_services_package_unittests($host_toolchain)",
"//topaz/public/dart/sledge:dart_sledge_tests($host_toolchain)",
diff --git a/public/dart/fuchsia_modular_flutter/BUILD.gn b/public/dart/fuchsia_modular_flutter/BUILD.gn
new file mode 100644
index 0000000..c103b17
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/BUILD.gn
@@ -0,0 +1,47 @@
+# 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("//build/dart/dart_library.gni")
+import("//topaz/runtime/dart/flutter_test.gni")
+
+dart_library("fuchsia_modular_flutter") {
+ package_name = "fuchsia_modular_flutter"
+
+ sdk_category = "partner"
+
+ sources = [
+ "session_shell.dart",
+ "src/session_shell.dart",
+ "src/story.dart",
+ "src/session_shell/internal/_focus_request_watcher_impl.dart",
+ "src/session_shell/internal/_focus_watcher_impl.dart",
+ "src/session_shell/internal/_modular_session_shell_impl.dart",
+ "src/session_shell/internal/_session_shell_impl.dart",
+ "src/session_shell/internal/_session_shell_presentation_provider_impl.dart",
+ "src/session_shell/internal/_story_provider_watcher_impl.dart",
+ ]
+
+ deps = [
+ "//sdk/fidl/fuchsia.modular",
+ "//sdk/fidl/fuchsia.ui.views",
+ "//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//third_party/dart-pkg/pub/meta",
+ "//topaz/public/dart/fuchsia_scenic_flutter",
+ "//topaz/public/dart/fuchsia_services",
+ ]
+}
+
+# Runs these tests using:
+# fx run-host-tests fuchsia_modular_flutter_unittests
+flutter_test("fuchsia_modular_flutter_unittests") {
+ sources = [
+ "session_shell_test.dart",
+ ]
+
+ deps = [
+ ":fuchsia_modular_flutter",
+ "//third_party/dart-pkg/pub/mockito",
+ "//third_party/dart-pkg/pub/test",
+ ]
+}
\ No newline at end of file
diff --git a/public/dart/fuchsia_modular_flutter/OWNERS b/public/dart/fuchsia_modular_flutter/OWNERS
new file mode 100644
index 0000000..2c4be4b
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/OWNERS
@@ -0,0 +1,3 @@
+sanjayc@google.com
+anwilson@google.com
+*
diff --git a/public/dart/fuchsia_modular_flutter/analysis_options.yaml b/public/dart/fuchsia_modular_flutter/analysis_options.yaml
new file mode 100644
index 0000000..bebf512
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/analysis_options.yaml
@@ -0,0 +1,5 @@
+# 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.
+
+include: ../../analysis_options.yaml
diff --git a/public/dart/fuchsia_modular_flutter/lib/session_shell.dart b/public/dart/fuchsia_modular_flutter/lib/session_shell.dart
new file mode 100644
index 0000000..9166b01
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/session_shell.dart
@@ -0,0 +1,7 @@
+// 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.
+
+/// A collection of utilities simplifying Session Shell creation.
+export 'src/session_shell.dart';
+export 'src/story.dart';
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell.dart
new file mode 100644
index 0000000..c9fe35f
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell.dart
@@ -0,0 +1,73 @@
+// 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:fidl_fuchsia_ui_policy/fidl_async.dart' show Presentation;
+import 'package:fidl_fuchsia_modular/fidl_async.dart'
+ show SessionShellContext, PuppetMaster;
+import 'package:fuchsia_services/services.dart' show StartupContext;
+import 'package:meta/meta.dart';
+
+import 'session_shell/internal/_session_shell_impl.dart';
+import 'story.dart';
+
+/// 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. Only the
+/// [onStoryStarted] callback is required, since it returns a concrete [Story].
+abstract class SessionShell {
+ static SessionShell _sessionShell;
+
+ /// Returns a shared instance of this.
+ factory SessionShell({
+ @required StartupContext startupContext,
+ @required StoryFactory onStoryStarted,
+ StoryCallback onStoryChanged,
+ StoryCallback onStoryDeleted,
+ }) {
+ return _sessionShell ??= SessionShellImpl(
+ startupContext: startupContext,
+ onStoryStarted: onStoryStarted,
+ onStoryChanged: onStoryChanged,
+ onStoryDeleted: onStoryDeleted,
+ );
+ }
+
+ /// An interable for stories in the session.
+ Iterable<Story> get stories;
+
+ /// The [Story] that is currently focused. It could be [null].
+ Story get focusedStory;
+
+ /// Returns the [SessionShellContext].
+ SessionShellContext get context;
+
+ /// Returns the [Presentation] used by the session.
+ Presentation get presentation;
+
+ /// Returns the [PuppetMaster] used by the session.
+ PuppetMaster get puppetMaster;
+
+ /// Register this instance of Session Shell with modular framework. This
+ /// needs to be called before any other methods.
+ void start();
+
+ /// Unregister and disconnect from modular framework.
+ void stop();
+
+ /// Request focus for story with [id]. Ensure [start] is called before
+ /// invoking this method. Otherwise this is a no-op.
+ void focusStory(String id);
+
+ /// Delete the [Story] given the id. Ensure [start] is called before
+ /// invoking this method. Otherwise this is a no-op.
+ void deleteStory(String id);
+
+ /// Stop the story with the id. Ensure [start] is called before
+ /// invoking this method. Otherwise this is a no-op.
+ void stopStory(String id);
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_request_watcher_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_request_watcher_impl.dart
new file mode 100644
index 0000000..92710a3
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_request_watcher_impl.dart
@@ -0,0 +1,21 @@
+// 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 'dart:async';
+
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+
+/// Extends [modular.FocusRequestWatcher]. This class passes the request to
+/// set focus on a story to the session shell.
+class FocusRequestWatcherImpl extends modular.FocusRequestWatcher {
+ final void Function(String) _onFocusRequestCallback;
+
+ /// Constructor.
+ FocusRequestWatcherImpl(this._onFocusRequestCallback);
+
+ @override
+ Future<void> onFocusRequest(String storyId) async {
+ _onFocusRequestCallback?.call(storyId);
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_watcher_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_watcher_impl.dart
new file mode 100644
index 0000000..eaa9ca9
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_focus_watcher_impl.dart
@@ -0,0 +1,21 @@
+// 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 'dart:async';
+
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+
+/// Extends [modular.FocusWatcher]. Notifies session shell when focus switches
+/// to another story.
+class FocusWatcherImpl extends modular.FocusWatcher {
+ final void Function(modular.FocusInfo) _onFocusChangeCallback;
+
+ /// Constructor.
+ FocusWatcherImpl(this._onFocusChangeCallback);
+
+ @override
+ Future<void> onFocusChange(modular.FocusInfo focusInfo) async {
+ _onFocusChangeCallback?.call(focusInfo);
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_modular_session_shell_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_modular_session_shell_impl.dart
new file mode 100644
index 0000000..39875c3
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_modular_session_shell_impl.dart
@@ -0,0 +1,32 @@
+// 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 'dart:async';
+
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewHolderToken;
+
+/// Extends [modular.SessionShell]. Notifies session shell when a story's view
+/// is attached and detached.
+class ModularSessionShellImpl extends modular.SessionShell {
+ final void Function(
+ modular.ViewIdentifier,
+ ViewHolderToken,
+ ) _attachView2Callback;
+ final void Function(modular.ViewIdentifier) _detachViewCallback;
+
+ /// Constructor.
+ ModularSessionShellImpl(this._attachView2Callback, this._detachViewCallback);
+
+ @override
+ Future<void> attachView2(
+ modular.ViewIdentifier viewId, ViewHolderToken viewHolderToken) async {
+ _attachView2Callback?.call(viewId, viewHolderToken);
+ }
+
+ @override
+ Future<void> detachView(modular.ViewIdentifier viewId) async {
+ _detachViewCallback?.call(viewId);
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_impl.dart
new file mode 100644
index 0000000..5392903
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_impl.dart
@@ -0,0 +1,318 @@
+// 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);
+ }
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_presentation_provider_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_presentation_provider_impl.dart
new file mode 100644
index 0000000..b52c2c8
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_session_shell_presentation_provider_impl.dart
@@ -0,0 +1,39 @@
+// 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 'dart:async';
+
+import 'package:fidl/fidl.dart' show InterfaceRequest, InterfaceHandle;
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+import 'package:fidl_fuchsia_ui_policy/fidl_async.dart' show Presentation;
+
+/// Extends [modular.SessionShellPresentationProvider]. This class forwards the
+/// request to get the session shell's presentation and watch a story's visual
+/// state.
+class SessionShellPresentationProviderImpl
+ extends modular.SessionShellPresentationProvider {
+ final void Function(InterfaceRequest<Presentation>) _presentationCallback;
+ final void Function(String, InterfaceHandle<modular.StoryVisualStateWatcher>)
+ _watchVisualStateCallback;
+
+ /// Constructor.
+ SessionShellPresentationProviderImpl(
+ this._presentationCallback, this._watchVisualStateCallback);
+
+ @override
+ Future<void> getPresentation(
+ String storyId,
+ InterfaceRequest<Presentation> request,
+ ) async {
+ _presentationCallback(request);
+ }
+
+ @override
+ Future<void> watchVisualState(
+ String storyId,
+ InterfaceHandle<modular.StoryVisualStateWatcher> watcherHandle,
+ ) async {
+ _watchVisualStateCallback(storyId, watcherHandle);
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_story_provider_watcher_impl.dart b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_story_provider_watcher_impl.dart
new file mode 100644
index 0000000..f286d02
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/session_shell/internal/_story_provider_watcher_impl.dart
@@ -0,0 +1,35 @@
+// 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 'dart:async';
+
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+
+/// Extends [modular.StoryProviderWatcher]. Notifies the session shell when a
+/// story's state changes or a story is deleted.
+class StoryProviderWatcherImpl extends modular.StoryProviderWatcher {
+ final void Function(
+ modular.StoryInfo,
+ modular.StoryState,
+ modular.StoryVisibilityState,
+ ) _onChangeCallback;
+ final void Function(String) _onDeleteCallback;
+
+ /// Constructor.
+ StoryProviderWatcherImpl(this._onChangeCallback, this._onDeleteCallback);
+
+ @override
+ Future<void> onChange(
+ modular.StoryInfo storyInfo,
+ modular.StoryState storyState,
+ modular.StoryVisibilityState storyVisibilityState,
+ ) async {
+ _onChangeCallback?.call(storyInfo, storyState, storyVisibilityState);
+ }
+
+ @override
+ Future<void> onDelete(String storyId) async {
+ _onDeleteCallback?.call(storyId);
+ }
+}
diff --git a/public/dart/fuchsia_modular_flutter/lib/src/story.dart b/public/dart/fuchsia_modular_flutter/lib/src/story.dart
new file mode 100644
index 0000000..52bedf5
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/lib/src/story.dart
@@ -0,0 +1,37 @@
+// 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:fidl_fuchsia_modular/fidl_async.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+
+/// Defines a class that represents a 'story' in Fuchsia. It holds state that
+/// describe the current runtime characterstics of the story:
+abstract class Story {
+ /// Returns the unique id of the story.
+ String get id;
+
+ /// Returns the [StoryInfo] of the story.
+ StoryInfo get info;
+
+ /// Holds the focused state of the story.
+ bool focused;
+
+ /// Holds the runtime [StoryState] of the story.
+ StoryState state;
+
+ /// Holds the [StoryVisibilityState] of the story.
+ StoryVisibilityState visibilityState;
+
+ /// Holds the [ChildViewConnection] assigned to the story.
+ ChildViewConnection childViewConnection;
+
+ /// Request focus on this story instance.
+ void focus();
+
+ /// Stop the story.
+ void stop();
+
+ /// Delete this story instance.
+ void delete();
+}
diff --git a/public/dart/fuchsia_modular_flutter/pubspec.yaml b/public/dart/fuchsia_modular_flutter/pubspec.yaml
new file mode 100644
index 0000000..5c74da8
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/pubspec.yaml
@@ -0,0 +1,7 @@
+# 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.
+
+name: fuchsia_modular_flutter
+environment:
+ sdk: '>=2.0.0 <3.0.0'
diff --git a/public/dart/fuchsia_modular_flutter/test/session_shell_test.dart b/public/dart/fuchsia_modular_flutter/test/session_shell_test.dart
new file mode 100644
index 0000000..6519526
--- /dev/null
+++ b/public/dart/fuchsia_modular_flutter/test/session_shell_test.dart
@@ -0,0 +1,213 @@
+// 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:test/test.dart';
+
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
+import 'package:fuchsia_modular_flutter/session_shell.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore: implementation_imports
+import 'package:fuchsia_modular_flutter/src/session_shell/internal/_session_shell_impl.dart';
+
+void main() {
+ test('Create SessionShell', () {
+ expect(
+ SessionShell(
+ startupContext: MockStartupContext(),
+ onStoryStarted: ({
+ modular.StoryInfo info,
+ SessionShell sessionShell,
+ modular.StoryController controller,
+ }) {
+ return TestStory()..info = info;
+ }),
+ isNotNull);
+ });
+
+ test('Start SessionShell', () {
+ final mockStartupContext = MockStartupContext();
+ final mockOutgoingImpl = MockOutgoing();
+ when(mockStartupContext.outgoing).thenReturn(mockOutgoingImpl);
+
+ TestSessionShell(startupContext: mockStartupContext).start();
+
+ verify(mockOutgoingImpl.addPublicService(
+ any, modular.SessionShell.$serviceName));
+ verify(mockOutgoingImpl.addPublicService(
+ any, modular.SessionShellPresentationProvider.$serviceName));
+ });
+
+ test('Story started', () {
+ String startedStoryId;
+ final sessionShell = TestSessionShell(onStoryStarted: ({
+ modular.StoryInfo info,
+ SessionShell sessionShell,
+ modular.StoryController controller,
+ }) {
+ startedStoryId = info.id;
+ return TestStory()..info = info;
+ })
+ ..onChange(
+ modular.StoryInfo(id: 'foo', lastFocusTime: 0),
+ modular.StoryState.stopped,
+ modular.StoryVisibilityState.default$,
+ );
+ expect(startedStoryId, 'foo');
+ verify(sessionShell.mockStoryController.requestStart());
+ });
+
+ test('Story deleted', () {
+ String deletedStoryId;
+ TestSessionShell(onStoryDeleted: (story) {
+ deletedStoryId = story.id;
+ })
+ ..onChange(
+ modular.StoryInfo(id: 'foo', lastFocusTime: 0),
+ modular.StoryState.stopped,
+ modular.StoryVisibilityState.default$,
+ )
+ ..deleteStory('foo');
+ expect(deletedStoryId, 'foo');
+ });
+
+ test('Story changed', () {
+ Story changedStory;
+ TestSessionShell(onStoryChanged: (story) {
+ changedStory = story;
+ })
+ ..onChange(
+ modular.StoryInfo(id: 'foo', lastFocusTime: 0),
+ modular.StoryState.stopped,
+ modular.StoryVisibilityState.default$,
+ )
+ ..onChange(
+ modular.StoryInfo(id: 'foo', lastFocusTime: 0),
+ modular.StoryState.running,
+ modular.StoryVisibilityState.default$,
+ );
+ expect(changedStory.id, 'foo');
+ expect(changedStory.state, modular.StoryState.running);
+ });
+
+ test('Story focused', () {
+ Story changedStory;
+ TestSessionShell(onStoryChanged: (story) {
+ changedStory = story;
+ })
+ ..onChange(
+ modular.StoryInfo(id: 'foo', lastFocusTime: 0),
+ modular.StoryState.stopped,
+ modular.StoryVisibilityState.default$,
+ )
+ ..onFocusChange(modular.FocusInfo(
+ focusedStoryId: 'foo',
+ deviceId: '',
+ lastFocusChangeTimestamp: 0,
+ ));
+ expect(changedStory.id, 'foo');
+ expect(changedStory.focused, true);
+ });
+}
+
+class TestSessionShell extends SessionShellImpl {
+ MockSessionShellContext mockSessionShellContext = MockSessionShellContext();
+ MockStoryProvider mockStoryProvider = MockStoryProvider();
+ MockPuppetMaster mockPuppetMaster = MockPuppetMaster();
+ MockStoryController mockStoryController = MockStoryController();
+
+ TestSessionShell({
+ StartupContext startupContext,
+ StoryFactory onStoryStarted = _onStoryStarted,
+ StoryCallback onStoryChanged,
+ StoryCallback onStoryDeleted,
+ }) : super(
+ startupContext: startupContext ?? MockStartupContext(),
+ onStoryStarted: onStoryStarted,
+ onStoryDeleted: onStoryDeleted,
+ onStoryChanged: onStoryChanged,
+ );
+
+ static Story _onStoryStarted({
+ modular.StoryInfo info,
+ SessionShell sessionShell,
+ modular.StoryController controller,
+ }) {
+ return TestStory()..info = info;
+ }
+
+ @override
+ modular.SessionShellContext get context => mockSessionShellContext;
+
+ @override
+ modular.StoryProvider get storyProvider => mockStoryProvider;
+
+ @override
+ modular.PuppetMaster get puppetMaster => mockPuppetMaster;
+
+ @override
+ modular.StoryControllerProxy newStoryController() {
+ final mockCtrl = MockAsyncProxyController<modular.StoryController>();
+ when(mockStoryController.ctrl).thenReturn(mockCtrl);
+ return mockStoryController;
+ }
+
+ @override
+ void watch(
+ modular.StoryProvider storyProvider,
+ modular.SessionShellContext context,
+ ) {}
+}
+
+class TestStory implements Story {
+ @override
+ ChildViewConnection childViewConnection;
+
+ @override
+ bool focused;
+
+ @override
+ modular.StoryState state;
+
+ @override
+ modular.StoryVisibilityState visibilityState;
+
+ @override
+ void delete() {}
+
+ @override
+ void focus() {}
+
+ @override
+ String get id => info.id;
+
+ @override
+ modular.StoryInfo info;
+
+ @override
+ void stop() {}
+}
+
+// Mock classes.
+class MockStartupContext extends Mock implements StartupContext {}
+
+class MockOutgoing extends Mock implements Outgoing {}
+
+class MockIncomping extends Mock implements Incoming {}
+
+class MockSessionShellContext extends Mock
+ implements modular.SessionShellContext {}
+
+class MockStoryProvider extends Mock implements modular.StoryProvider {}
+
+class MockPuppetMaster extends Mock implements modular.PuppetMaster {}
+
+class MockStoryController extends Mock implements modular.StoryControllerProxy {
+}
+
+class MockAsyncProxyController<T> extends Mock
+ implements AsyncProxyController<T> {}