croupier: Add Sounds

Adds sounds to Croupier when your device plays a card or receives
a card. This also applies to the (large) board view.

The board view was also slightly modified since the sizing seemed
to be a little off in portrait mode.

Note: Flutter does not officially support the sound system in Mojo
Shell. Therefore, what has occurred here has been hacked in.

What one must change to get MojoShell to play Flutter sounds is:
In packages/flutter_sprites/src/sound.dart,
change shell.connectToService(null, _mediaService);
to shell.connectToService("mojo:media_service", _mediaService);

There are corresponding changes that have to be made in your fork
of Mojo. Essentially, you would be copying the files from:
* engine/sky/services/media/BUILD.gn
* engine/sky/services/media/src/org/domokit/*.java

and put them into your fork of Mojo. (Some massaging is needed, since
BUILD files are hard.)

Deploy and shortcuts were updated too.

Change-Id: I4083484f2be82492bf6dd7b1d49c0e7283e58ee1
diff --git a/Makefile b/Makefile
index 24f222a..2dc7152 100644
--- a/Makefile
+++ b/Makefile
@@ -143,6 +143,9 @@
 DISCOVERY_MOJO_DIR := $(PWD)/packages/v23discovery/mojo_services
 GS_BUCKET_PATH := gs://mojo_services
 
+# Note: The deploy assumes that the media_service.mojo file, which is used for
+# audio, is already present at $(GS_BUCKET_PATH)/media_service.mojo.
+# This file is developed separately from Croupier.
 .PHONY: deploy
 deploy: build
 	gsutil cp $(APP_ICON) $(GS_BUCKET_PATH)/croupier
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 27fd7a5..09a3b68 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -11,6 +11,7 @@
 import '../logic/croupier.dart' show Croupier;
 import '../logic/game/game.dart' show Game, GameType, NoArgCb;
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase;
+import '../sound/sound_assets.dart';
 import '../styles/common.dart' as style;
 import 'card.dart' as component_card;
 import 'card_collection.dart'
@@ -50,13 +51,14 @@
 /// cards each player has, and the cards they are currently playing.
 class HeartsBoard extends Board {
   final Croupier croupier;
+  final SoundAssets sounds;
   final bool isMini;
   final AcceptCb gameAcceptCallback;
   final List<logic_card.Card> bufferedPlay;
 
   HeartsGame get game => super.game;
 
-  HeartsBoard(Croupier croupier,
+  HeartsBoard(Croupier croupier, this.sounds,
       {double height,
       double width,
       double cardHeight,
@@ -77,9 +79,88 @@
 }
 
 class HeartsBoardState extends State<HeartsBoard> {
+  static const double PROFILE_SIZE = 0.18; // multiplier of config.height
+
+  // Every time the counter changes, a sound will be played.
+  // For example, in the pass/take phase, the counter does this:
+  // 0->1->2->3->4->3->2->1->0.
+  // We play 4 whooshIn sounds followed by 4 whooshOut sounds upon detecting
+  // the change. Each sound only occurs during the very first build (the first
+  // opportunity to detect the change).
+  // In the play phase, we have this instead: 0->1->2->3->4->0
+  // This 5-cycle is 4 played cards (whooshIn) and 1 take trick (whooshOut).
+  int cardCounter = 0;
+  bool passing = true;
+
+  void _handleCardCounterSounds() {
+    // Ensure we have the right state while we deal and score.
+    if (config.game.phase == HeartsPhase.Deal ||
+        config.game.phase == HeartsPhase.Score) {
+      cardCounter = 0;
+      passing = true;
+    }
+
+    // Passing
+    if (passing) {
+      // If it is now someone's turn, we should no longer be passing.
+      if (config.game.whoseTurn != null) {
+        passing = false;
+
+        // Special: Play a sound for the last take command of the pass phase.
+        if (cardCounter > 0) {
+          cardCounter = 0;
+          _playSoundOut();
+        }
+        return;
+      }
+
+      // Passing: If somebody passed cards recently...
+      if (config.game.numPassed > cardCounter) {
+        cardCounter = config.game.numPassed;
+        _playSoundIn();
+        return;
+      }
+
+      // Passing: If somebody took cards recently...
+      if (config.game.numPassed < cardCounter) {
+        cardCounter = config.game.numPassed;
+        _playSoundOut();
+        return;
+      }
+      return;
+    }
+
+    // Playing: If somebody played a card...
+    if (config.game.numPlayed > cardCounter) {
+      cardCounter = config.game.numPlayed;
+      _playSoundIn();
+      return;
+    }
+
+    // Playing: If somebody took the trick...
+    if (config.game.numPlayed == 0 && cardCounter != 0) {
+      cardCounter = 0;
+      _playSoundOut();
+    }
+  }
+
+  void _playSoundIn() {
+    if (!config.isMini) {
+      config.sounds.play("whooshIn");
+    }
+  }
+
+  void _playSoundOut() {
+    if (!config.isMini) {
+      config.sounds.play("whooshOut");
+    }
+  }
+
   Widget build(BuildContext context) {
     double offscreenDelta = config.isMini ? 5.0 : 1.5;
 
+    _handleCardCounterSounds();
+
     Widget boardChild;
     if (config.game.phase == HeartsPhase.Play) {
       boardChild =
@@ -203,21 +284,21 @@
         height: config.height,
         width: config.width,
         child: new Column([
-          new Flexible(child: _playerProfile(2, 0.2), flex: 0),
+          new Flexible(child: _playerProfile(2, PROFILE_SIZE), flex: 0),
           new Flexible(child: _getPass(2), flex: 0),
           new Flexible(
               child: new Row([
-                new Flexible(child: _playerProfile(1, 0.2), flex: 0),
+                new Flexible(child: _playerProfile(1, PROFILE_SIZE), flex: 0),
                 new Flexible(child: _getPass(1), flex: 0),
                 new Flexible(child: new Block([]), flex: 1),
                 new Flexible(child: _getPass(3), flex: 0),
-                new Flexible(child: _playerProfile(3, 0.2), flex: 0)
+                new Flexible(child: _playerProfile(3, PROFILE_SIZE), flex: 0)
               ],
                   alignItems: FlexAlignItems.center,
                   justifyContent: FlexJustifyContent.spaceAround),
               flex: 1),
           new Flexible(child: _getPass(0), flex: 0),
-          new Flexible(child: _playerProfile(0, 0.2), flex: 0)
+          new Flexible(child: _playerProfile(0, PROFILE_SIZE), flex: 0)
         ],
             alignItems: FlexAlignItems.center,
             justifyContent: FlexJustifyContent.spaceAround));
@@ -315,21 +396,21 @@
         height: config.height,
         width: config.width,
         child: new Column([
-          new Flexible(child: _playerProfile(2, 0.2), flex: 0),
+          new Flexible(child: _playerProfile(2, PROFILE_SIZE), flex: 0),
           new Flexible(child: _showTrickText(2), flex: 0),
           new Flexible(
               child: new Row([
-                new Flexible(child: _playerProfile(1, 0.2), flex: 0),
+                new Flexible(child: _playerProfile(1, PROFILE_SIZE), flex: 0),
                 new Flexible(child: _showTrickText(1), flex: 0),
                 new Flexible(child: _buildCenterCards(), flex: 1),
                 new Flexible(child: _showTrickText(3), flex: 0),
-                new Flexible(child: _playerProfile(3, 0.2), flex: 0)
+                new Flexible(child: _playerProfile(3, PROFILE_SIZE), flex: 0)
               ],
                   alignItems: FlexAlignItems.center,
                   justifyContent: FlexJustifyContent.spaceAround),
               flex: 1),
           new Flexible(child: _showTrickText(0), flex: 0),
-          new Flexible(child: _playerProfile(0, 0.2), flex: 0)
+          new Flexible(child: _playerProfile(0, PROFILE_SIZE), flex: 0)
         ],
             alignItems: FlexAlignItems.center,
             justifyContent: FlexJustifyContent.spaceAround));
@@ -362,8 +443,15 @@
   }
 
   double get _centerScaleFactor {
-    return math.min(config.height * 0.6 / (config.cardHeight * 3),
-        config.width - config.height * 0.4 / (config.cardWidth * 3));
+    bool wide = (config.width >= config.height);
+    double heightUsage = (1 - 2 * PROFILE_SIZE);
+
+    if (wide) {
+      return config.height * heightUsage / (config.cardHeight * 3);
+    } else {
+      return (config.width - config.height * heightUsage) /
+          (config.cardWidth * 3);
+    }
   }
 
   Widget _buildCenterCard(int playerNumber) {
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 61b73b4..ffb5664 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -9,6 +9,7 @@
 import '../logic/croupier.dart' as logic_croupier;
 import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' as logic_game;
+import '../sound/sound_assets.dart';
 import '../styles/common.dart' as style;
 import 'croupier_game_advertisement.dart'
     show CroupierGameAdvertisementComponent;
@@ -21,8 +22,9 @@
 
 class CroupierComponent extends StatefulComponent {
   final logic_croupier.Croupier croupier;
+  final SoundAssets sounds;
 
-  CroupierComponent(this.croupier);
+  CroupierComponent(this.croupier, this.sounds);
 
   CroupierComponentState createState() => new CroupierComponentState();
 }
@@ -125,7 +127,9 @@
       case logic_croupier.CroupierState.PlayGame:
         return new Container(
             padding: new EdgeDims.only(top: ui.window.padding.top),
-            child: component_game.createGameComponent(config.croupier,
+            child: component_game.createGameComponent(
+                config.croupier,
+                config.sounds,
                 makeSetStateCallback(logic_croupier.CroupierState.Welcome),
                 width: ui.window.size.width,
                 height: ui.window.size.height - ui.window.padding.top,
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 677a9c8..bbe1bcf 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -23,6 +23,7 @@
 import 'card_collection.dart'
     show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb;
 import 'croupier_profile.dart' show CroupierProfileComponent;
+import '../sound/sound_assets.dart';
 
 part 'hearts/hearts.part.dart';
 part 'proto/proto.part.dart';
@@ -32,12 +33,13 @@
 
 abstract class GameComponent extends StatefulComponent {
   final Croupier croupier;
+  final SoundAssets sounds;
   Game get game => croupier.game;
   final NoArgCb gameEndCallback;
   final double width;
   final double height;
 
-  GameComponent(this.croupier, this.gameEndCallback,
+  GameComponent(this.croupier, this.sounds, this.gameEndCallback,
       {Key key, this.width, this.height})
       : super(key: key);
 }
@@ -200,17 +202,18 @@
   }
 }
 
-GameComponent createGameComponent(Croupier croupier, NoArgCb gameEndCallback,
+GameComponent createGameComponent(
+    Croupier croupier, SoundAssets sounds, NoArgCb gameEndCallback,
     {Key key, double width, double height}) {
   switch (croupier.game.gameType) {
     case GameType.Proto:
-      return new ProtoGameComponent(croupier, gameEndCallback,
+      return new ProtoGameComponent(croupier, sounds, gameEndCallback,
           key: key, width: width, height: height);
     case GameType.Hearts:
-      return new HeartsGameComponent(croupier, gameEndCallback,
+      return new HeartsGameComponent(croupier, sounds, gameEndCallback,
           key: key, width: width, height: height);
     case GameType.Solitaire:
-      return new SolitaireGameComponent(croupier, gameEndCallback,
+      return new SolitaireGameComponent(croupier, sounds, gameEndCallback,
           key: key, width: width, height: height);
     default:
       // We're probably not ready to serve the other games yet.
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 319c87c..f69b819 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -5,9 +5,9 @@
 part of game_component;
 
 class HeartsGameComponent extends GameComponent {
-  HeartsGameComponent(Croupier croupier, NoArgCb cb,
+  HeartsGameComponent(Croupier croupier, SoundAssets sounds, NoArgCb cb,
       {Key key, double width, double height})
-      : super(croupier, cb, key: key, width: width, height: height);
+      : super(croupier, sounds, cb, key: key, width: width, height: height);
 
   HeartsGame get game => super.game;
 
@@ -223,6 +223,7 @@
       try {
         config.game.passCards(_combinePassing());
         config.game.debugString = null;
+        config.sounds.play("whooshOut");
       } catch (e) {
         print("You can't do that! ${e.toString()}");
         config.game.debugString = "You must pass 3 cards";
@@ -240,6 +241,7 @@
         _clearPassing();
         config.game.takeCards();
         config.game.debugString = null;
+        config.sounds.play("whooshIn");
       } catch (e) {
         print("You can't do that! ${e.toString()}");
         config.game.debugString = e.toString();
@@ -262,6 +264,7 @@
           bufferedPlay.add(card);
         } else {
           game.move(card, dest);
+          config.sounds.play("whooshOut");
         }
         game.debugString = null;
       } else {
@@ -271,6 +274,15 @@
     });
   }
 
+  void _makeTakeTrickCallback() {
+    HeartsGame game = config.game;
+    setState(() {
+      game.takeTrickUI();
+      game.debugString = null;
+      config.sounds.play("whooshIn");
+    });
+  }
+
   void _endRoundDebugCallback() {
     setState(() {
       config.game.jumpToScorePhaseDebug();
@@ -352,8 +364,8 @@
   }
 
   Widget showBoard() {
-    return new HeartsBoard(config.croupier,
-        width: config.width, height: 0.80 * config.height);
+    return new HeartsBoard(config.croupier, config.sounds,
+        width: config.width, height: 0.825 * config.height);
   }
 
   String _getName(int playerNumber) {
@@ -445,12 +457,8 @@
             game.determineTrickWinner() == game.playerNumber) {
           statusBarWidgets.add(new Flexible(
               flex: 0,
-              child: new GestureDetector(onTap: () {
-                setState(() {
-                  game.takeTrickUI();
-                  game.debugString = null;
-                });
-              },
+              child: new GestureDetector(
+                  onTap: _makeTakeTrickCallback,
                   child: new Container(
                       decoration: style.Box.brightBackground,
                       margin: style.Spacing.smallPaddingSide,
@@ -512,7 +520,7 @@
     return new Container(
         width: config.width * 0.5,
         height: config.height * 0.25,
-        child: new HeartsBoard(config.croupier,
+        child: new HeartsBoard(config.croupier, config.sounds,
             width: config.width * 0.5,
             height: config.height * 0.25,
             cardWidth: config.height * 0.1,
diff --git a/lib/components/main_route.dart b/lib/components/main_route.dart
index 7db8c8c..0c402bd 100644
--- a/lib/components/main_route.dart
+++ b/lib/components/main_route.dart
@@ -8,13 +8,15 @@
 import '../styles/common.dart' as style;
 import 'croupier.dart' show CroupierComponent;
 import 'croupier_profile.dart' show CroupierProfileComponent;
+import '../sound/sound_assets.dart';
 
 final GlobalKey _scaffoldKey = new GlobalKey();
 
 class MainRoute extends StatefulComponent {
   final Croupier croupier;
+  final SoundAssets sounds;
 
-  MainRoute(this.croupier);
+  MainRoute(this.croupier, this.sounds);
 
   MainRouteState createState() => new MainRouteState();
 }
@@ -47,7 +49,8 @@
                 icon: "navigation/menu",
                 onPressed: () => _scaffoldKey.currentState?.openDrawer()),
             center: new Text('Croupier')),
-        body: new Material(child: new CroupierComponent(config.croupier)),
+        body: new Material(
+            child: new CroupierComponent(config.croupier, config.sounds)),
         drawer: _buildDrawer());
   }
 
diff --git a/lib/components/proto/proto.part.dart b/lib/components/proto/proto.part.dart
index 94a84e5..b9fb48f 100644
--- a/lib/components/proto/proto.part.dart
+++ b/lib/components/proto/proto.part.dart
@@ -5,9 +5,9 @@
 part of game_component;
 
 class ProtoGameComponent extends GameComponent {
-  ProtoGameComponent(Croupier croupier, NoArgCb cb,
+  ProtoGameComponent(Croupier croupier, SoundAssets sounds, NoArgCb cb,
       {Key key, double width, double height})
-      : super(croupier, cb, key: key, width: width, height: height);
+      : super(croupier, sounds, cb, key: key, width: width, height: height);
 
   ProtoGameComponentState createState() => new ProtoGameComponentState();
 }
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index 75e4d58..4ede22e 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -5,9 +5,9 @@
 part of game_component;
 
 class SolitaireGameComponent extends GameComponent {
-  SolitaireGameComponent(Croupier croupier, NoArgCb cb,
+  SolitaireGameComponent(Croupier croupier, SoundAssets sounds, NoArgCb cb,
       {Key key, double width, double height})
-      : super(croupier, cb, key: key, width: width, height: height);
+      : super(croupier, sounds, cb, key: key, width: width, height: height);
 
   SolitaireGameComponentState createState() =>
       new SolitaireGameComponentState();
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index e4759ed..f4bb0e5 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -209,10 +209,17 @@
 
   bool hasPassed(int player) =>
       cardCollections[player + OFFSET_PASS].length == 3;
-  bool get allPassed => cardCollections[PLAYER_A_PASS].length == 3 &&
-      cardCollections[PLAYER_B_PASS].length == 3 &&
-      cardCollections[PLAYER_C_PASS].length == 3 &&
-      cardCollections[PLAYER_D_PASS].length == 3;
+  int get numPassed {
+    int count = 0;
+    for (int i = 0; i < 4; i++) {
+      if (cardCollections[i + OFFSET_PASS].length == 3) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  bool get allPassed => numPassed == 4;
   bool hasTaken(int player) =>
       cardCollections[getTakeTarget(player) + OFFSET_PASS].length == 0;
   bool get allTaken => cardCollections[PLAYER_A_PASS].length == 0 &&
diff --git a/lib/main.dart b/lib/main.dart
index 4cd2bfd..8087ea3 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,18 +2,22 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'package:flutter/material.dart';
 import 'dart:async';
 
-import 'settings/client.dart' as settings_client;
-import 'logic/croupier.dart' show Croupier;
-import 'components/settings_route.dart' show SettingsRoute;
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
 import 'components/main_route.dart' show MainRoute;
+import 'components/settings_route.dart' show SettingsRoute;
+import 'logic/croupier.dart' show Croupier;
+import 'settings/client.dart' as settings_client;
+import 'sound/sound_assets.dart';
 import 'styles/common.dart' as style;
 
 class CroupierApp extends StatefulComponent {
   settings_client.AppSettings appSettings;
-  CroupierApp(this.appSettings);
+  SoundAssets sounds;
+  CroupierApp(this.appSettings, this.sounds);
 
   CroupierAppState createState() => new CroupierAppState();
 }
@@ -30,18 +34,35 @@
     return new MaterialApp(
         title: 'Croupier',
         routes: <String, RouteBuilder>{
-          "/": (RouteArguments args) => new MainRoute(croupier),
+          "/": (RouteArguments args) => new MainRoute(croupier, config.sounds),
           "/settings": (RouteArguments args) => new SettingsRoute(croupier)
         },
         theme: style.theme);
   }
 }
 
+AssetBundle _initBundle() {
+  // Note: Code was copied from parts of Flutter that load assets like sound.
+  // rootBundle comes from flutter/services.dart
+  if (rootBundle != null) return rootBundle;
+  return new NetworkAssetBundle(new Uri.directory(Uri.base.origin));
+}
+
+Future<SoundAssets> loadAudio() async {
+  final AssetBundle _bundle = _initBundle();
+  SoundAssets _sounds = new SoundAssets(_bundle);
+
+  // Load sounds in parallel.
+  await Future.wait([_sounds.load("whooshIn"), _sounds.load("whooshOut")]);
+  return _sounds;
+}
+
 void main() {
   // TODO(alexfandrianto): Perhaps my app will run better if I initialize more
   // things here instead of in Croupier. I added this 500 ms delay because the
   // tablet was sometimes not rendering without it (repainting too early?).
   new Future.delayed(const Duration(milliseconds: 500), () async {
-    runApp(new CroupierApp(await settings_client.getSettings()));
+    runApp(new CroupierApp(
+        await settings_client.getSettings(), await loadAudio()));
   });
 }
diff --git a/lib/sound/sound_assets.dart b/lib/sound/sound_assets.dart
new file mode 100644
index 0000000..bf5f2d4
--- /dev/null
+++ b/lib/sound/sound_assets.dart
@@ -0,0 +1,28 @@
+// Copyright 2015 The Vanadium 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:flutter/services.dart';
+import 'package:flutter_sprites/flutter_sprites.dart';
+
+/// SoundAssets are used to play sounds in the game.
+class SoundAssets {
+  SoundAssets(this._bundle) {
+    _soundEffectPlayer = new SoundEffectPlayer(20);
+  }
+
+  AssetBundle _bundle;
+  SoundEffectPlayer _soundEffectPlayer;
+  Map<String, SoundEffect> _soundEffects = <String, SoundEffect>{};
+
+  Future load(String name) async {
+    _soundEffects[name] =
+        await _soundEffectPlayer.load(await _bundle.load('sounds/$name.wav'));
+  }
+
+  void play(String name) {
+    _soundEffectPlayer.play(_soundEffects[name]);
+  }
+}
diff --git a/manifest.yaml b/manifest.yaml
index cb3f666..5918e6b 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -255,3 +255,5 @@
   - images/splash/background.png
   - images/splash/flutter.png
   - images/splash/vanadium.png
+  - sounds/whooshIn.wav
+  - sounds/whooshOut.wav
diff --git a/pubspec.lock b/pubspec.lock
index 25a0d88..e1c978b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -20,7 +20,7 @@
   async:
     description: async
     source: hosted
-    version: "1.5.0"
+    version: "1.6.0"
   barback:
     description: barback
     source: hosted
@@ -29,6 +29,10 @@
     description: bignum
     source: hosted
     version: "0.0.7"
+  box2d:
+    description: box2d
+    source: hosted
+    version: "0.2.0"
   cassowary:
     description:
       path: "../../../flutter/packages/cassowary"
@@ -77,6 +81,12 @@
       relative: true
     source: path
     version: "0.0.21"
+  flutter_sprites:
+    description:
+      path: "../../../flutter/packages/flutter_sprites"
+      relative: true
+    source: path
+    version: "0.0.15"
   flutter_tools:
     description:
       path: "../../../flutter/packages/flutter_tools"
@@ -230,7 +240,7 @@
   stack_trace:
     description: stack_trace
     source: hosted
-    version: "1.5.0"
+    version: "1.5.1"
   string_scanner:
     description: string_scanner
     source: hosted
@@ -238,7 +248,7 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.26"
+    version: "0.0.27"
   test:
     description: test
     source: hosted
@@ -254,7 +264,7 @@
   v23discovery:
     description: v23discovery
     source: hosted
-    version: "0.0.8"
+    version: "0.0.9"
   vector_math:
     description: vector_math
     source: hosted
diff --git a/pubspec.yaml b/pubspec.yaml
index 57b6e04..e1bdb0e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,6 +4,8 @@
   syncbase: ">=0.0.0 <0.1.0"
   flutter:
     path: ../../../flutter/packages/flutter
+  flutter_sprites:
+    path: ../../../flutter/packages/flutter_sprites
   flutter_tools:
     path: ../../../flutter/packages/flutter_tools
   test: any
diff --git a/shortcut_template b/shortcut_template
index 5df960b..015b1db 100644
--- a/shortcut_template
+++ b/shortcut_template
@@ -1,5 +1,6 @@
---map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/e5c866cd9573c4a8fe893ddcf196be5aaef4df38/android-arm/
---url-mappings=mojo:flutter=http://flutter/flutter.mojo
+--map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/90ef9fa39c36f4027b82e62262e5c0c43a0466a1/android-arm/
+--url-mappings=mojo:flutter=http://flutter/flutter.mojo,mojo:media_service=https://mojo3.v.io/media_service.mojo
+--map-origin=https://mojo3.v.io=https://storage.googleapis.com/mojo_services/
 --debug
 --verbose
 --args-for=mojo:flutter --enable-checked-mode
diff --git a/sounds/whooshIn.wav b/sounds/whooshIn.wav
new file mode 100644
index 0000000..c928382
--- /dev/null
+++ b/sounds/whooshIn.wav
Binary files differ
diff --git a/sounds/whooshOut.wav b/sounds/whooshOut.wav
new file mode 100644
index 0000000..91ceeed
--- /dev/null
+++ b/sounds/whooshOut.wav
Binary files differ