[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> {}