Import Cobalt 19.lts.3.194831
diff --git a/src/cobalt/build/build.id b/src/cobalt/build/build.id
index 7c34122..3b17a37 100644
--- a/src/cobalt/build/build.id
+++ b/src/cobalt/build/build.id
@@ -1 +1 @@
-194115
\ No newline at end of file
+194831
\ No newline at end of file
diff --git a/src/cobalt/layout_tests/web_platform_tests.cc b/src/cobalt/layout_tests/web_platform_tests.cc
index 3227dc1..8317211 100644
--- a/src/cobalt/layout_tests/web_platform_tests.cc
+++ b/src/cobalt/layout_tests/web_platform_tests.cc
@@ -269,13 +269,7 @@
   if (test_server.empty()) {
     FilePath url_path = GetTestInputRootDirectory()
                         .Append(FILE_PATH_LITERAL("web-platform-tests"));
-#if defined(COBALT_LINUX) || defined(COBALT_WIN)
-    // Get corp configuration.
-    url_path = url_path.Append(FILE_PATH_LITERAL("corp.url"));
-#else
-    // Get lab configuration.
     url_path = url_path.Append(FILE_PATH_LITERAL("lab.url"));
-#endif
     file_util::ReadFileToString(url_path, &test_server);
     TrimWhitespaceASCII(test_server, TRIM_ALL, &test_server);
     ASSERT_FALSE(test_server.empty());
diff --git a/src/starboard/android/__init__.py b/src/starboard/android/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/__init__.py
diff --git a/src/starboard/android/apk/apk.gyp b/src/starboard/android/apk/apk.gyp
new file mode 100644
index 0000000..26e8f54
--- /dev/null
+++ b/src/starboard/android/apk/apk.gyp
@@ -0,0 +1,32 @@
+# Copyright 2018 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'targets': [
+    {
+      # This target generates a stamp file that the platform deploy action can
+      # depend on to re-run the Gradle build only when any file that's checked
+      # into git within the 'apk' directory changes.
+      'target_name': 'apk_sources',
+      'type': 'none',
+      'actions': [
+        {
+          'action_name': 'apk_sources',
+          'inputs': [ '<!@(git -C <(DEPTH)/starboard/android/apk ls-files)' ],
+          'outputs': [ '<(PRODUCT_DIR)/gradle/apk_sources.stamp' ],
+          'action': [ 'touch', '<@(_outputs)' ],
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/starboard/android/apk/app/CMakeLists.txt b/src/starboard/android/apk/app/CMakeLists.txt
new file mode 100644
index 0000000..3e0fd3c
--- /dev/null
+++ b/src/starboard/android/apk/app/CMakeLists.txt
@@ -0,0 +1,94 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Since libcoat.so is an "IMPORTED" library in CMake, in order to debug in
+# Android Studio you have to manually set the debugger type so that LLDB will
+# be started:
+# Run -> Edit Configurations -> "app" -> Debugger -> Debug Type = Dual
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Map Android ABI to Cobalt architecture
+if(ANDROID_ABI STREQUAL x86)
+  set(COBALT_ARCH x86)
+elseif(ANDROID_ABI STREQUAL armeabi-v7a)
+  set(COBALT_ARCH arm)
+elseif(ANDROID_ABI STREQUAL arm64-v8a)
+  set(COBALT_ARCH arm64)
+else()
+  message(SEND_ERROR "Unsupported Android ABI: ${ANDROID_ABI}.")
+endif()
+
+# If COBALT_PRODUCT_DIR isn't set use the relative path up to the appropriate
+# 'out' directory.
+if(NOT COBALT_PRODUCT_DIR)
+  set(COBALT_PRODUCT_DIR
+      ${CMAKE_CURRENT_LIST_DIR}/../../../../out/android-${COBALT_ARCH}_${COBALT_CONFIG}
+  )
+endif()
+
+# If COBALT_CONTENT_DIR isn't set for a particular deploy target use the
+# 'content/data' directory.
+if(NOT COBALT_CONTENT_DIR)
+  set(COBALT_CONTENT_DIR
+      ${COBALT_PRODUCT_DIR}/content/data
+  )
+endif()
+
+# For platform deploy builds, use the -n parameter to skip Cobalt ninja and
+# just copy the .so that was already built and waiting in COBALT_PRODUCT_DIR.
+if(COBALT_PLATFORM_DEPLOY)
+  set(skip_ninja_arg -n)
+endif()
+
+# Run Cobalt ninja, and copy the result as our "IMPORTED" libcoat.so.
+# ("coat_lib" never gets created, so this runs every time.)
+add_custom_command(OUTPUT coat_lib
+    COMMAND ${CMAKE_CURRENT_LIST_DIR}/cobalt-ninja.sh
+            ${skip_ninja_arg} -C ${COBALT_PRODUCT_DIR} ${COBALT_TARGET}
+    COMMAND ${CMAKE_COMMAND} -E copy
+            ${COBALT_PRODUCT_DIR}/lib/lib${COBALT_TARGET}.so
+            ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcoat.so
+)
+
+# Make a symlink to the cobalt static content. We put it in a parent directory
+# of the library output, which corresponds to what the Gradle build sets for
+# android.sourceSets.<build-type>.assets.srcDir. This ends up overwriting the
+# symlink for each ABI built, but that's okay since the Cobalt static content
+# for a given build config is the same for all architectures.
+# ("cobalt_content" never gets created, so this runs every time.)
+add_custom_command(OUTPUT cobalt_content
+    DEPENDS coat_lib
+    COMMAND ${CMAKE_COMMAND} -E create_symlink
+            ${COBALT_CONTENT_DIR}
+            ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../../cobalt_content
+)
+
+# We need a target (not a file) for the phony native dependency below.
+add_custom_target(external_cobalt_build DEPENDS coat_lib cobalt_content)
+
+# Declare libcoat.so as a shared library that needs to be included in the APK.
+# However, Android Studio will build it as an "IMPORTED" library.
+add_library(coat SHARED IMPORTED)
+set_target_properties(coat PROPERTIES
+    IMPORTED_LOCATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcoat.so
+)
+
+# Make a phony "native" library, so Android Studio has something to build.
+file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/phony.cpp "void __phony() {}")
+add_library(native SHARED  ${CMAKE_CURRENT_BINARY_DIR}/phony.cpp)
+
+# Add a dependency to run the external cobalt build as a side effect of
+# building the phony native library.
+add_dependencies(native external_cobalt_build)
diff --git a/src/starboard/android/apk/app/build.gradle b/src/starboard/android/apk/app/build.gradle
new file mode 100644
index 0000000..ad09589
--- /dev/null
+++ b/src/starboard/android/apk/app/build.gradle
@@ -0,0 +1,162 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+apply plugin: 'com.android.application'
+
+final DEFAULT_COBALT_TARGET = 'cobalt'
+final String[] SUPPORTED_ABIS = [ 'x86', 'armeabi-v7a', 'arm64-v8a' ]
+
+ext {
+    // Provide defaults if these properties aren't specified on the command line.
+    buildAbi = project.hasProperty('cobaltBuildAbi') ? cobaltBuildAbi : SUPPORTED_ABIS
+    cobaltTarget = project.hasProperty('cobaltTarget') ? cobaltTarget : DEFAULT_COBALT_TARGET
+    cobaltProductDir =
+            project.hasProperty('cobaltProductDir') ? new File(cobaltProductDir).canonicalPath : ''
+    cobaltContentDir =
+            project.hasProperty('cobaltContentDir') ? new File(cobaltContentDir).canonicalPath : ''
+
+    buildIdFile = rootProject.file('build.id')
+    buildId = buildIdFile.exists() ? buildIdFile.text.trim() : '0'
+}
+
+println "TARGET: ${cobaltTarget}"
+
+android {
+    compileSdkVersion 28
+    buildToolsVersion "28.0.3"
+
+    signingConfigs {
+        // A signing config that matches what is implicitly used for the "debug" build type.
+        debugConfig {
+            keyAlias 'androiddebugkey'
+            keyPassword 'android'
+            storeFile file("${System.getProperty('user.home')}/.android/debug.keystore")
+            storePassword 'android'
+        }
+    }
+    defaultConfig {
+        applicationId "dev.cobalt.coat"
+        minSdkVersion 21
+        targetSdkVersion 28
+        versionCode 1
+        versionName "${buildId}"
+        manifestPlaceholders = [applicationName: "CoAT: ${cobaltTarget}"]
+        externalNativeBuild {
+            cmake {
+                arguments "-DCOBALT_TARGET=${cobaltTarget}"
+                arguments "-DCOBALT_PRODUCT_DIR=${cobaltProductDir}"
+                arguments "-DCOBALT_CONTENT_DIR=${cobaltContentDir}"
+                arguments "-DCOBALT_PLATFORM_DEPLOY=${project.hasProperty('cobaltDeployApk')}"
+            }
+        }
+    }
+    splits {
+        abi {
+            enable true
+            reset()
+            include buildAbi
+        }
+    }
+    buildTypes {
+        debug {
+            debuggable true
+            jniDebuggable true
+            externalNativeBuild {
+                cmake.arguments "-DCOBALT_CONFIG=debug"
+            }
+            signingConfig signingConfigs.debugConfig
+        }
+        devel {
+            debuggable true
+            jniDebuggable true
+            externalNativeBuild {
+                cmake.arguments "-DCOBALT_CONFIG=devel"
+            }
+            signingConfig signingConfigs.debugConfig
+        }
+        qa {
+            debuggable true
+            jniDebuggable true
+            externalNativeBuild {
+                cmake.arguments "-DCOBALT_CONFIG=qa"
+            }
+            signingConfig signingConfigs.debugConfig
+        }
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+            externalNativeBuild {
+                cmake.arguments "-DCOBALT_CONFIG=gold"
+            }
+            signingConfig signingConfigs.debugConfig
+        }
+    }
+    sourceSets {
+        // The source is split into two parts:
+        //   "main" is the code that should go into any Cobalt-based app for Android TV.
+        //   "app" is the specialization needed to make "the blue app" out of the main code.
+        // The Android Gradle plugin includes "main" by default, so we just need to add "app".
+        main {
+            manifest.srcFile "src/app/AndroidManifest.xml"
+            java.srcDir "src/app/java"
+            res.srcDir "src/app/res"
+            assets.srcDir "src/app/assets"
+        }
+        // Add the directories symlinked by the CMake "cobalt_content" custom command.
+        debug {
+            assets.srcDir "${buildDir}/intermediates/cmake/debug/cobalt_content"
+        }
+        devel {
+            assets.srcDir "${buildDir}/intermediates/cmake/devel/cobalt_content"
+        }
+        qa {
+            assets.srcDir "${buildDir}/intermediates/cmake/qa/cobalt_content"
+        }
+        release {
+            assets.srcDir "${buildDir}/intermediates/cmake/release/cobalt_content"
+        }
+    }
+    externalNativeBuild {
+        cmake {
+            path 'CMakeLists.txt'
+            if (project.hasProperty('cobaltGradleDir')) {
+                // Resolve relative to the current dir at config time by getting a canonical File.
+                buildStagingDirectory new File(cobaltGradleDir, 'externalNativeBuild').canonicalFile
+            }
+        }
+    }
+}
+
+// When running in the platform deploy action, make tasks that depend on the appropriate Android
+// assemble tasks, then the move the assembled "app" apk to the desired path for deployment.
+if (project.hasProperty('cobaltDeployApk')) {
+    android.applicationVariants.all { variant ->
+        task "assembleCobalt_${variant.name}"(dependsOn: "assemble${variant.name.capitalize()}") {
+            def assembledApk = variant.outputs[0].outputFile
+            def deployApk = new File(cobaltDeployApk).canonicalFile
+            doLast {
+                assembledApk.renameTo(deployApk)
+            }
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation 'com.android.support:support-annotations:28.0.0'
+    implementation 'com.android.support:leanback-v17:28.0.0'
+    implementation 'com.android.support:support-v4:28.0.0'
+    implementation 'com.google.android.gms:play-services-auth:16.0.1'
+    implementation 'com.google.protobuf:protobuf-lite:3.0.1'
+}
diff --git a/src/starboard/android/apk/app/cobalt-ninja.sh b/src/starboard/android/apk/app/cobalt-ninja.sh
new file mode 100755
index 0000000..6dc1375
--- /dev/null
+++ b/src/starboard/android/apk/app/cobalt-ninja.sh
@@ -0,0 +1,47 @@
+#! /bin/bash
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This wrapper allows us to run ninja without any assumptions about the
+# environment having been setup correctly to build Cobalt, since that won't
+# happen when started from Android Studio. See:
+# https://cobalt.googlesource.com/cobalt/+/master/src/#Building-and-Running-the-Code
+
+# Allow for a developer-specific environment setup from .cobaltrc
+# e.g., it may set DEPOT_TOOLS and/or setup some distributed build tools.
+local_rc=$(dirname $0)/.cobaltrc
+global_rc=${HOME}/.cobaltrc
+if [ -r ${local_rc} ]; then
+  source ${local_rc}
+elif [ -r ${global_rc} ]; then
+  source ${global_rc}
+fi
+
+# DEPOT_TOOLS may be set in .cobaltrc, otherwise assume it's in $HOME.
+[ -x ${DEPOT_TOOLS}/ninja ] || DEPOT_TOOLS=${HOME}/depot_tools
+
+# Use Cobalt's clang if it's not anywhere earlier in the PATH.
+SRC_DIR=$(cd $(dirname $0)/../../../..; pwd)
+PATH=$PATH:${SRC_DIR}/third_party/llvm-build/Release+Asserts/bin
+
+# Do nothing if -n is the first argument.
+if [ "$1" == "-n" ]; then
+  shift
+  echo "Skipping: ninja $@"
+  exit
+fi
+
+# When running in CMake, depot_tools isn't in the path, so we have to be
+# explicit about which ninja to run. Fail if we didn't find depot_tools.
+exec ${DEPOT_TOOLS}/ninja "$@"
diff --git a/src/starboard/android/apk/app/proguard-rules.pro b/src/starboard/android/apk/app/proguard-rules.pro
new file mode 100644
index 0000000..a4b8a54
--- /dev/null
+++ b/src/starboard/android/apk/app/proguard-rules.pro
@@ -0,0 +1,35 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Proguard flags needed for the Android Starboard Java implementation to work
+# properly with its JNI native library.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# JNI is an entry point that's hard to keep track of, so the @UsedByNative
+# annotation marks fields and methods used by native code.
+
+# Annotations are implemented as attributes, so we have to explicitly keep them.
+-keepattributes *Annotation*
+
+# Keep classes, methods, and fields that are accessed with JNI.
+-keep @interface dev.cobalt.util.UsedByNative
+-keep @dev.cobalt.util.UsedByNative class *
+-keepclasseswithmembers class * {
+  @dev.cobalt.util.UsedByNative <methods>;
+}
+-keepclasseswithmembers class * {
+  @dev.cobalt.util.UsedByNative <fields>;
+}
diff --git a/src/starboard/android/apk/app/src/app/AndroidManifest.xml b/src/starboard/android/apk/app/src/app/AndroidManifest.xml
new file mode 100644
index 0000000..7337495
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/AndroidManifest.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="dev.cobalt.coat">
+
+  <uses-feature android:name="android.hardware.microphone" android:required="false"/>
+  <uses-feature android:name="android.software.leanback" android:required="false"/>
+  <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
+  <uses-feature android:glEsVersion="0x00020000" android:required="true"/>
+
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+  <uses-permission android:name="android.permission.USE_CREDENTIALS" android:maxSdkVersion="22"/>
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
+  <application
+    android:name="dev.cobalt.app.CobaltApplication"
+    android:icon="@mipmap/ic_app"
+    android:banner="@drawable/app_banner"
+    android:label="${applicationName}">
+
+    <activity android:name="dev.cobalt.app.MainActivity"
+      android:launchMode="singleTask"
+      android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode"
+      android:screenOrientation="sensorLandscape"
+      android:theme="@style/CobaltTheme">
+      <meta-data android:name="cobalt.APP_URL" android:value="https://www.youtube.com/tv"/>
+      <meta-data android:name="cobalt.SPLASH_URL" android:value="h5vcc-embedded://cobalt_splash_screen.html"/>
+      <meta-data android:name="android.app.lib_name" android:value="coat"/>
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+        <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+      </intent-filter>
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW"/>
+        <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <category android:name="android.intent.category.BROWSABLE"/>
+        <data android:scheme="http"/>
+        <data android:scheme="https"/>
+        <data android:host="youtube.com"/>
+        <data android:host="www.youtube.com"/>
+        <data android:host="m.youtube.com"/>
+        <data android:host="youtu.be"/>
+        <data android:pathPattern=".*"/>
+      </intent-filter>
+    </activity>
+
+  </application>
+
+</manifest>
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css
new file mode 100644
index 0000000..e3ca851
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.css
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 The Cobalt Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+body {
+  overflow: hidden;
+  font-size: 1.4815vh;  /* Corresponds to 16px at 1080p. */
+}
+
+#splash {
+  background-color: #16396b;
+  background-image: url("file:///cobalt_logo_1024.png");
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: auto 33%;
+  height: 100%;
+  left: 0;
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+
+#loading {
+  position: absolute;
+  top: 52em;
+  width: 100%;
+}
+
+#spinner {
+  /* The spinner starts with display set to none, and JavaScript will set this
+     to 'block' after some time has passed, if the splash screen is still
+     visible. */
+  display: none;
+
+  height: 5.33em;
+  margin: 0 auto;
+  position: relative;
+  width: 5.33em;
+}
+
+.dot {
+  background-color: #cbcbcb;
+  border-radius: 50%;
+  height: 1.17em;
+  position: absolute;
+  width: 1.17em;
+}
+
+@keyframes fade1 {
+  0%,100% {opacity: 0}
+  50% {opacity: 1}
+}
+
+@keyframes fade2 {
+  0%,100% {opacity: .25}
+  37.5% {opacity: 1}
+  87.5% {opacity: 0}
+}
+
+@keyframes fade3 {
+  0%,100% {opacity: .5}
+  25% {opacity: 1}
+  75% {opacity: 0}
+}
+
+@keyframes fade4 {
+  0%,100% {opacity: .75}
+  12.5% {opacity: 1}
+  62.5% {opacity: 0}
+}
+
+@keyframes fade5 {
+  0%,100% {opacity: 1}
+  50% {opacity: 0}
+}
+
+@keyframes fade6 {
+  0%,100% {opacity: .75}
+  37.5% {opacity: 0}
+  87.5% {opacity: 1}
+}
+
+@keyframes fade7 {
+  0%,100% {opacity: .5}
+  25% {opacity: 0}
+  75% {opacity: 1}
+}
+
+@keyframes fade8 {
+  0%,100% {opacity: .25}
+  12.5% {opacity: 0}
+  62.5% {opacity: 1}
+}
+
+#dot1 {
+  animation: fade8 .72s infinite ease;
+  left: 0;
+  top: 2.09em;
+}
+
+#dot2 {
+  animation: fade7 .72s infinite ease;
+  left: .61em;
+  top: .61em;
+}
+
+#dot3 {
+  animation: fade6 .72s infinite ease;
+  left: 2.09em;
+  top: 0;
+}
+
+#dot4 {
+  animation: fade5 .72s infinite ease;
+  right: .61em;
+  top: .61em;
+}
+
+#dot5 {
+  animation: fade4 .72s infinite ease;
+  right: 0;
+  top: 2.09em;
+}
+
+#dot6 {
+  animation: fade3 .72s infinite ease;
+  bottom: .61em;
+  right: .61em;
+}
+
+#dot7 {
+  animation: fade2 .72s infinite ease;
+  bottom: 0;
+  left: 2.09em;
+}
+
+#dot8 {
+  animation: fade1 .72s infinite ease;
+  bottom: .61em;
+  left: .61em;
+}
+
+.hidden {
+  height: 0;
+  visibility: hidden;
+}
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html
new file mode 100644
index 0000000..3a453a6
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_blue_splash_screen.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<html>
+
+<head>
+  <meta http-equiv="Content-Security-Policy" content="default-src 'none';
+      script-src h5vcc-embedded://*/splash_screen.js;
+      style-src file://*/cobalt_blue_splash_screen.css;
+      img-src file://*/cobalt_logo_1024.png;">
+<link rel="stylesheet" type="text/css"
+    href="file:///cobalt_blue_splash_screen.css">
+</head>
+
+<body>
+  <div id="splash">
+    <img src="file:///cobalt_logo_1024.png" class="hidden">
+  </div>
+  <div id="loading">
+    <div id="spinner">
+      <div class="dot" id="dot1"></div>
+      <div class="dot" id="dot2"></div>
+      <div class="dot" id="dot3"></div>
+      <div class="dot" id="dot4"></div>
+      <div class="dot" id="dot5"></div>
+      <div class="dot" id="dot6"></div>
+      <div class="dot" id="dot7"></div>
+      <div class="dot" id="dot8"></div>
+    </div>
+  </div>
+  <script type="text/javascript" src="h5vcc-embedded://splash_screen.js">
+  </script>
+</body>
+
+</html>
diff --git a/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png b/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png
new file mode 100644
index 0000000..443a571
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/cobalt_logo_1024.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html b/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html
new file mode 100644
index 0000000..4aa1686
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/assets/web/link_android_splash_screen.html
@@ -0,0 +1,691 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<html>
+
+<head>
+  <meta http-equiv="Content-Security-Policy" content="
+    default-src 'unsafe-inline';
+    script-src file://*/cobalt_blue_splash_screen.html;
+    style-src 'unsafe-inline';"
+</head>
+
+<body style="background-color: #1f52a5;">
+  <link rel="splashscreen" href="file:///cobalt_blue_splash_screen.html">
+<h1>Heading</h1>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
+interdum maximus finibus. Ut fermentum malesuada commodo. Sed
+faucibus, sapien a mattis lobortis, magna ante efficitur mauris, quis
+sodales nibh diam nec quam. Vestibulum magna libero, tincidunt non
+erat sed, molestie pulvinar ex. Maecenas semper blandit elit, id
+suscipit nulla venenatis pretium. Integer accumsan porta felis, vitae
+placerat urna accumsan vel. Aliquam eu aliquet mi. Aenean tincidunt
+eros lectus, sit amet efficitur orci ultrices at. Morbi lobortis ex
+quis luctus rutrum. In nulla velit, elementum vitae turpis vitae,
+finibus varius massa. Morbi id libero faucibus, tempus eros et,
+ullamcorper ipsum. Sed eleifend finibus bibendum. Nullam ut nunc nec
+lacus posuere dignissim. Nunc sollicitudin vitae augue id
+vulputate. Ut ac nibh gravida, volutpat est ac, facilisis neque.</p>
+
+<p>Nam dictum leo massa, non posuere dui bibendum id. Morbi sagittis est
+non est laoreet, a sollicitudin felis aliquet. Ut cursus vel leo a
+efficitur. Proin ut pellentesque sapien, vel maximus dui. Suspendisse
+eu felis eget leo elementum efficitur. Class aptent taciti sociosqu ad
+litora torquent per conubia nostra, per inceptos himenaeos. Fusce
+lobortis velit in elit pellentesque, ut auctor ipsum dignissim. Sed
+aliquet eleifend convallis. Duis mollis, dolor sed rutrum mollis,
+augue eros dignissim erat, eu dapibus augue turpis ac sapien. Morbi at
+volutpat odio, at molestie risus. Nulla quis nulla et magna vestibulum
+euismod. Praesent suscipit quam elit, non luctus turpis rutrum
+faucibus.</p>
+
+<p>Morbi feugiat lacus rhoncus, dignissim velit nec, dignissim
+lorem. Aliquam erat volutpat. Mauris semper dictum tempus. Nulla ex
+ligula, malesuada in ornare sed, euismod vitae massa. Etiam quis erat
+quis nisl facilisis suscipit. Mauris placerat ante et auctor
+fermentum. Donec tincidunt justo sem, ullamcorper vulputate nisl
+commodo a. Vestibulum quis ex non elit porttitor semper eget quis
+tortor. Suspendisse mattis neque non elementum scelerisque. Nulla
+facilisi. Nulla non felis et justo feugiat elementum. Aenean sodales
+turpis at erat eleifend lacinia. Proin eleifend volutpat purus id
+mollis. Proin vel tellus faucibus, sagittis libero at, lobortis
+odio. Praesent quam mauris, auctor vel velit eu, convallis molestie
+nisi. Pellentesque in nunc at orci ultrices vehicula.</p>
+
+<p>Praesent nibh lectus, efficitur sed risus in, rutrum tristique
+arcu. Curabitur non efficitur elit. Phasellus eget odio iaculis,
+molestie dui eget, venenatis erat. Nulla luctus facilisis lectus, nec
+dapibus tortor rhoncus vel. Donec nec arcu elit. Nullam ut faucibus
+purus, sed ultricies diam. Pellentesque at finibus ipsum. Vestibulum
+egestas dignissim nisl, ac rhoncus risus finibus sit amet. Donec non
+feugiat ante. Donec vehicula dui a lorem imperdiet, a tempus diam
+pulvinar. Nullam congue efficitur justo, non posuere ligula sodales
+in. Ut a urna ornare, ultrices velit in, pellentesque
+lorem. Vestibulum ante ipsum primis in faucibus orci luctus et
+ultrices posuere cubilia Curae;</p>
+
+<p>Orci varius natoque penatibus et magnis dis parturient montes,
+nascetur ridiculus mus. Morbi maximus quis magna et aliquet. Nam
+bibendum fermentum tempus. Praesent iaculis tortor metus, at
+vestibulum ipsum hendrerit mattis. Proin fringilla nisl sit amet
+tincidunt blandit. Interdum et malesuada fames ac ante ipsum primis in
+faucibus. Phasellus vel lectus leo. Curabitur fringilla, arcu non
+posuere viverra, urna metus blandit augue, convallis mattis tortor dui
+vel arcu. In sit amet metus vitae ex rhoncus hendrerit.</p>
+
+</body>
+</html>
diff --git a/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java
new file mode 100644
index 0000000..83f0a8e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java
@@ -0,0 +1,35 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.app;
+
+import android.app.Application;
+import dev.cobalt.coat.StarboardBridge;
+
+/**
+ * Android Application hosting the Starboard application.
+ */
+public class CobaltApplication extends Application implements StarboardBridge.HostApplication {
+  StarboardBridge starboardBridge;
+
+  @Override
+  public void setStarboardBridge(StarboardBridge starboardBridge) {
+    this.starboardBridge = starboardBridge;
+  }
+
+  @Override
+  public StarboardBridge getStarboardBridge() {
+    return starboardBridge;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java
new file mode 100644
index 0000000..04e70a8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java
@@ -0,0 +1,53 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.app;
+
+import android.app.Activity;
+import dev.cobalt.account.UserAuthorizerImpl;
+import dev.cobalt.coat.CobaltActivity;
+import dev.cobalt.coat.StarboardBridge;
+import dev.cobalt.feedback.NoopFeedbackService;
+import dev.cobalt.util.Holder;
+
+/**
+ * Main Activity for the "Cobalt on Android TV" app.
+ *
+ * <p>The real work is done in the abstract base class. This class is really just some factory
+ * methods to "inject" things that can be customized.
+ */
+public class MainActivity extends CobaltActivity {
+
+  @Override
+  protected StarboardBridge createStarboardBridge(String[] args, String startDeepLink) {
+    Holder<Activity> activityHolder = new Holder<>();
+    Runnable stopRequester =
+        new Runnable() {
+          @Override
+          public void run() {
+            getStarboardBridge().requestStop(0);
+          }
+        };
+    UserAuthorizerImpl userAuthorizer =
+        new UserAuthorizerImpl(getApplicationContext(), activityHolder, stopRequester);
+    NoopFeedbackService feedbackService = new NoopFeedbackService();
+    return new StarboardBridge(
+        getApplicationContext(),
+        activityHolder,
+        userAuthorizer,
+        feedbackService,
+        args,
+        startDeepLink);
+  }
+}
diff --git a/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png b/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png
new file mode 100644
index 0000000..fd47a4d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/drawable-xhdpi/app_banner.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png
new file mode 100644
index 0000000..44469e7
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-hdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png
new file mode 100644
index 0000000..8cd39ca
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-mdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png
new file mode 100644
index 0000000..4e12e56
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-xhdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png b/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png
new file mode 100644
index 0000000..3948b1e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/mipmap-xxhdpi/ic_app.png
Binary files differ
diff --git a/src/starboard/android/apk/app/src/app/res/values/strings.xml b/src/starboard/android/apk/app/src/app/res/values/strings.xml
new file mode 100644
index 0000000..4b54b94
--- /dev/null
+++ b/src/starboard/android/apk/app/src/app/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+  <string name="app_name">Cobalt On Android TV</string>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java
new file mode 100644
index 0000000..9139b43
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/AccessToken.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * POJO holding the token for a signed-in user.
+ */
+public class AccessToken {
+
+  private final String tokenValue;
+  private final long expirySeconds;
+
+  public AccessToken(String tokenValue, long expirySeconds) {
+    this.tokenValue = tokenValue;
+    this.expirySeconds = expirySeconds;
+  }
+
+  /**
+   * Returns the token value.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public String getTokenValue() {
+    return tokenValue;
+  }
+
+  /**
+   * Returns number of seconds since epoch when this token expires, or 0 if it doesn't expire.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public long getExpirySeconds() {
+    return expirySeconds;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java
new file mode 100644
index 0000000..e995dd2
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/NoopUserAuthorizer.java
@@ -0,0 +1,59 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import android.content.Intent;
+import dev.cobalt.util.UsedByNative;
+
+/** UserAuthorizer implementation that doesn't try to sign in. */
+public class NoopUserAuthorizer implements UserAuthorizer {
+
+  @Override
+  public void shutdown() {}
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void interrupt() {}
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public AccessToken authorizeUser() {
+    return null;
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean deauthorizeUser() {
+    return false;
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public AccessToken refreshAuthorization() {
+    return null;
+  }
+
+  @Override
+  public void onActivityResult(int requestCode, int resultCode, Intent data) {}
+
+  @Override
+  public void onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {}
+
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java
new file mode 100644
index 0000000..1fc849e
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizer.java
@@ -0,0 +1,86 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import android.content.Intent;
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * Java side implementation for starboard::android::shared::cobalt::AndroidUserAuthorizer.
+ */
+public interface UserAuthorizer {
+
+  /**
+   * Cleans up at the end of the object's lifecycle.
+   */
+  void shutdown();
+
+  /**
+   * Unblocks any pending request.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void interrupt();
+
+  /**
+   * Prompts the user as necessary to choose an account, then gets an auth token for the selected
+   * account. I.e., pairs the web app to be signed-in as a selected account.
+   *
+   * This method blocks until finished, and must be called off the UI thread.
+   *
+   * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  AccessToken authorizeUser();
+
+  /**
+   * Prompts the user as necessary to stop using an account. i.e., unpairs the web app to be
+   * signed-out.
+   *
+   * This method blocks until finished, and must be called off the UI thread.
+   *
+   * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  boolean deauthorizeUser();
+
+  /**
+   * Gets a new auth token for the account most recently authorized.
+   *
+   * No UI will be shown if there is already a current account restored at construction or selected
+   * in authorizeUser(). If there is no current account, or the restored account has been deleted,
+   * the account picker may be shown to let the user specify which account should become the current
+   * account.
+   *
+   * This method blocks until finished, and must be called off the UI thread.
+   *
+   * Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  AccessToken refreshAuthorization();
+
+  /**
+   * @see android.app.Activity#onActivityResult(int, int, Intent)
+   */
+  void onActivityResult(int requestCode, int resultCode, Intent data);
+
+  /**
+   * @see android.app.Activity#onRequestPermissionsResult(int, String[], int[])
+   */
+  void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java
new file mode 100644
index 0000000..136da8b
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/account/UserAuthorizerImpl.java
@@ -0,0 +1,532 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.account;
+
+import static android.Manifest.permission.GET_ACCOUNTS;
+import static dev.cobalt.util.Log.TAG;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.google.android.gms.auth.GoogleAuthException;
+import com.google.android.gms.auth.GoogleAuthUtil;
+import com.google.android.gms.auth.UserRecoverableAuthException;
+import com.google.android.gms.common.AccountPicker;
+import dev.cobalt.coat.R;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.io.IOException;
+
+/**
+ * Java side implementation for starboard::android::shared::cobalt::AndroidUserAuthorizer.
+ *
+ * This implements the following business logic:
+ * First run...
+ * - if there are no accounts, just be be signed-out
+ * - if there is one account, sign-in without any UI
+ * - if there are more than one accounts, prompt to choose account
+ * Subsequent runs...
+ * - sign-in to the same account last used to sign-in
+ * - if previously signed-out stay signed-out
+ * When user clicks 'sign-in' in the UI...
+ * - if there are no accounts, allow user to add an account
+ * - if there is one account, sign-in without any UI
+ * - if there are more than one accounts, prompt to choose account
+ * If the last signed-in account is deleted...
+ * - kill the app if stopped in the background to prompt next time it starts
+ * - at the next app start, show a toast that the account isn't available
+ * - if there are no accounts left, just be be signed-out
+ * - if there are one or more accounts left, prompt to choose account
+ */
+public class UserAuthorizerImpl implements OnAccountsUpdateListener, UserAuthorizer {
+
+  /** Pseudo account indicating the user chose to be signed-out. */
+  public static final Account SIGNED_OUT_ACCOUNT = new Account("-", "-");
+
+  /** Pseudo account indicating a saved account no longer exists. */
+  private static final Account MISSING_ACCOUNT = new Account("!", "!");
+
+  /** Foreshortened expiry of Google OAuth token, which typically lasts 1 hour. */
+  private static final long DEFAULT_EXPIRY_SECONDS = 5 * 60;
+
+  private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+  private static final String[] OAUTH_SCOPES = {
+      "https://www.googleapis.com/auth/youtube"
+  };
+
+  private static final String SHARED_PREFS_NAME = "user_auth";
+  private static final String ACCOUNT_NAME_PREF_KEY = "signed_in_account";
+
+  /** The thread on which the current request is running, or null if none. */
+  private volatile Thread requestThread;
+
+  private final Context appContext;
+  private final Holder<Activity> activityHolder;
+  private final Runnable stopRequester;
+  private final Handler mainHandler;
+
+  private Account currentAccount = null;
+  private AccessToken currentToken = null;
+
+  // Result from the account picker UI lands here.
+  private String chosenAccountName;
+
+  private volatile boolean waitingForPermission;
+  private volatile boolean permissionGranted;
+
+  public UserAuthorizerImpl(
+      Context appContext, Holder<Activity> activityHolder, Runnable stopRequester) {
+    this.appContext = appContext;
+    this.activityHolder = activityHolder;
+    this.stopRequester = stopRequester;
+    this.mainHandler = new Handler(Looper.getMainLooper());
+    addOnAccountsUpdatedListener(this);
+  }
+
+  @Override
+  public void shutdown() {
+    removeOnAccountsUpdatedListener(this);
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void interrupt() {
+    Thread t = requestThread;
+    if (t != null) {
+      t.interrupt();
+    }
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public AccessToken authorizeUser() {
+    ensureBackgroundThread();
+    requestThread = Thread.currentThread();
+    // Let the user choose an account, or add one if there are none to choose.
+    // However, if there's only one account just choose it without any prompt.
+    currentAccount = autoSelectOrAddAccount();
+    writeAccountPref(currentAccount);
+    AccessToken accessToken = refreshCurrentToken();
+    requestThread = null;
+    return accessToken;
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean deauthorizeUser() {
+    ensureBackgroundThread();
+    requestThread = Thread.currentThread();
+    currentAccount = SIGNED_OUT_ACCOUNT;
+    writeAccountPref(currentAccount);
+    clearCurrentToken();
+    requestThread = null;
+    return true;
+  }
+
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public AccessToken refreshAuthorization() {
+    ensureBackgroundThread();
+    requestThread = Thread.currentThread();
+
+    // If we haven't yet determined which account to use, check preferences for a saved account.
+    if (currentAccount == null) {
+      Account savedAccount = readAccountPref();
+      if (savedAccount == null) {
+        // No saved account, so this is the first ever run of the app.
+        currentAccount = autoSelectAccount();
+      } else if (savedAccount.equals(MISSING_ACCOUNT)) {
+        // The saved account got deleted.
+        currentAccount = forceSelectAccount();
+      } else {
+        // Use the saved account.
+        currentAccount = savedAccount;
+      }
+      writeAccountPref(currentAccount);
+    }
+
+    AccessToken accessToken = refreshCurrentToken();
+    requestThread = null;
+    return accessToken;
+  }
+
+  private static void ensureBackgroundThread() {
+    if (Looper.myLooper() == Looper.getMainLooper()) {
+      throw new UnsupportedOperationException("UserAuthorizer can't be called on main thread");
+    }
+  }
+
+  private void showToast(int resId, Object... formatArgs) {
+    final String msg = appContext.getResources().getString(resId, formatArgs);
+    mainHandler.post(new Runnable() {
+      @Override
+      public void run() {
+        Toast.makeText(appContext, msg, Toast.LENGTH_LONG).show();
+      }
+    });
+  }
+
+  private Account readAccountPref() {
+    String savedAccountName = loadSignedInAccountName();
+    if (TextUtils.isEmpty(savedAccountName)) {
+      return null;
+    } else if (savedAccountName.equals(SIGNED_OUT_ACCOUNT.name)) {
+      // Don't request permissions or look for a device account if we were signed-out.
+      return SIGNED_OUT_ACCOUNT;
+    } else if (!checkPermission()) {
+      // We won't be able to get the account without permission, so warn the user and be signed-out.
+      showToast(R.string.starboard_missing_account, savedAccountName);
+      return SIGNED_OUT_ACCOUNT;
+    } else {
+      // Find the saved account name among all accounts on the device.
+      for (Account account : getAccounts()) {
+        if (account.name.equals(savedAccountName)) {
+          return account;
+        }
+      }
+      showToast(R.string.starboard_missing_account, savedAccountName);
+      return MISSING_ACCOUNT;
+    }
+  }
+
+  private void writeAccountPref(Account account) {
+    if (account == null) {
+      return;
+    }
+    // Always write the account name, even if it's the signed-out pseudo account.
+    saveSignedInAccountName(account.name);
+  }
+
+  private void clearCurrentToken() {
+    if (currentToken != null) {
+      clearToken(currentToken.getTokenValue());
+      currentToken = null;
+    }
+  }
+
+  private AccessToken refreshCurrentToken() {
+    clearCurrentToken();
+    if (currentAccount == null || SIGNED_OUT_ACCOUNT.equals(currentAccount)) {
+      return null;
+    }
+    String tokenValue = getToken(currentAccount);
+    if (tokenValue == null) {
+      showToast(R.string.starboard_account_auth_error);
+      tokenValue = "";
+    }
+    // TODO: Get the token details and use the actual expiry.
+    long expiry = System.currentTimeMillis() / 1000 + DEFAULT_EXPIRY_SECONDS;
+    currentToken = new AccessToken(tokenValue, expiry);
+    return currentToken;
+  }
+
+  /**
+   * Prompts the user to select an account, or to add an account if there are none. The prompt is
+   * skipped if there is exactly one account to choose from.
+   */
+  private Account autoSelectOrAddAccount() {
+    if (!checkPermission()) {
+      return SIGNED_OUT_ACCOUNT;
+    }
+    Account[] accounts = getAccounts();
+    if (accounts.length == 1) {
+      return accounts[0];
+    }
+    return selectOrAddAccount();
+  }
+
+  /**
+   * Prompts the user to select an account. The prompt is skipped if there are zero or one accounts
+   * to choose from.
+   */
+  private Account autoSelectAccount() {
+    if (!checkPermission()) {
+      return SIGNED_OUT_ACCOUNT;
+    }
+    Account[] accounts = getAccounts();
+    if (accounts.length == 0) {
+      return SIGNED_OUT_ACCOUNT;
+    } else if (accounts.length == 1) {
+      return accounts[0];
+    }
+    return selectOrAddAccount();
+  }
+
+  /**
+   * Prompts the user to select an account, even if there's only one to choose from. The prompt is
+   * skipped if there are zero accounts to choose from.
+   */
+  private Account forceSelectAccount() {
+    // We don't check permissions before calling selectOrAddAccount() because if the account is
+    // missing, readAccountPref() must have just checked, and we don't want to show permission
+    // prompt or rationale to the user twice.
+    Account[] accounts = getAccounts();
+    if (accounts.length == 0) {
+      return SIGNED_OUT_ACCOUNT;
+    }
+    return selectOrAddAccount();
+  }
+
+  /**
+   * Prompts the user to select an account, even if there's only one to choose from. If there are
+   * zero accounts to choose from, the user is prompted to add one.
+   *
+   * The caller should ensure permissions are granted before calling this method to avoid showing
+   * a picker with accounts that we can't access.
+   */
+  private Account selectOrAddAccount() {
+    String accountName = showAccountPicker();
+    // If user cancelled the picker stay signed-out.
+    if (TextUtils.isEmpty(accountName)) {
+      return SIGNED_OUT_ACCOUNT;
+    }
+    // Get the accounts after the picker in case one was added in the account picker.
+    for (Account account : getAccounts()) {
+      if (account.name.equals(accountName)) {
+        return account;
+      }
+    }
+    // This shouldn't happen, but if it does let the user know we're still signed-out.
+    Log.e(TAG, "Selected account is missing");
+    showToast(R.string.starboard_missing_account, accountName);
+    return SIGNED_OUT_ACCOUNT;
+  }
+
+  private synchronized String showAccountPicker() {
+    Activity activity = activityHolder.get();
+    Intent chooseAccountIntent = newChooseAccountIntent(currentAccount);
+    if (activity == null || chooseAccountIntent == null) {
+      return "";
+    }
+    chosenAccountName = null;
+    activity.startActivityForResult(chooseAccountIntent, R.id.rc_choose_account);
+
+    // Block until the account picker activity returns its result.
+    while (chosenAccountName == null) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Log.e(TAG, "Account picker interrupted");
+        // Return empty string, as if the picker was cancelled.
+        return "";
+      }
+    }
+    return chosenAccountName;
+  }
+
+  @Override
+  public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    if (requestCode == R.id.rc_choose_account) {
+      String accountName = null;
+      if (resultCode == Activity.RESULT_OK) {
+        accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+      } else if (resultCode != Activity.RESULT_CANCELED) {
+        Log.e(TAG, "Account picker error " + resultCode);
+        showToast(R.string.starboard_account_picker_error);
+      }
+
+      // Notify showAccountPicker() which account was chosen.
+      synchronized (this) {
+        // Return empty string if the picker is cancelled or there's an unexpected result.
+        chosenAccountName = (accountName == null) ? "" : accountName;
+        notifyAll();
+      }
+    }
+  }
+
+  @Override
+  public void onAccountsUpdated(Account[] unused) {
+    if (currentAccount == null || SIGNED_OUT_ACCOUNT.equals(currentAccount)) {
+      // We're not signed-in; the update doesn't affect us.
+      return;
+    }
+    // Call getAccounts() since the param may not match the accounts we can access.
+    for (Account account : getAccounts()) {
+      if (account.name.equals(currentAccount.name)) {
+        // The current account is still there; the update doesn't affect us.
+        return;
+      }
+    }
+    // The current account is gone; leave the app so we prompt for sign-in next time.
+    // This should only happen while stopped in the background since we don't delete accounts.
+    stopRequester.run();
+  }
+
+  /**
+   * Calls framework AccountManager.addOnAccountsUpdatedListener().
+   */
+  private void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+    AccountManager.get(appContext).addOnAccountsUpdatedListener(listener, null, false);
+  }
+
+  /**
+   * Calls framework AccountManager.removeOnAccountsUpdatedListener().
+   */
+  private void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+    AccountManager.get(appContext).removeOnAccountsUpdatedListener(listener);
+  }
+
+  /**
+   * Calls framework AccountManager.getAccountsByType() for Google accounts.
+   */
+  private Account[] getAccounts() {
+    return AccountManager.get(appContext).getAccountsByType(GOOGLE_ACCOUNT_TYPE);
+  }
+
+  /**
+   * Calls GMS AccountPicker.newChooseAccountIntent().
+   *
+   * Returns an Intent that when started will always show the account picker even if there's just
+   * one account on the device. If there are no accounts on the device it shows the UI to add one.
+   */
+  private Intent newChooseAccountIntent(Account defaultAccount) {
+    String[] allowableAccountTypes = {GOOGLE_ACCOUNT_TYPE};
+    return AccountPicker.newChooseAccountIntent(
+        defaultAccount, null, allowableAccountTypes, true, null, null, null, null);
+  }
+
+  /**
+   * Calls GMS GoogleAuthUtil.getToken(), without throwing any exceptions.
+   *
+   * Returns an empty string if no token is available for the account.
+   * Returns null if there was an error getting the token.
+   */
+  private String getToken(Account account) {
+    String joinedScopes = "oauth2:" + TextUtils.join(" ", OAUTH_SCOPES);
+    try {
+      return GoogleAuthUtil.getToken(appContext, account, joinedScopes);
+    } catch (UserRecoverableAuthException e) {
+        Log.w(TAG, "Recoverable error getting OAuth token", e);
+        Intent intent = e.getIntent();
+        Activity activity = activityHolder.get();
+        if (intent != null && activity != null) {
+          activity.startActivity(intent);
+        } else {
+          Log.e(TAG, "Failed to recover OAuth token", e);
+        }
+        return null;
+
+    } catch (IOException | GoogleAuthException e) {
+      Log.e(TAG, "Error getting auth token", e);
+      return null;
+    }
+  }
+
+  /**
+   * Calls GMS GoogleAuthUtil.clearToken(), without throwing any exceptions.
+   */
+  private void clearToken(String tokenValue) {
+    try {
+      GoogleAuthUtil.clearToken(appContext, tokenValue);
+    } catch (GoogleAuthException | IOException e) {
+      Log.e(TAG, "Error clearing auth token", e);
+    }
+  }
+
+  /**
+   * Checks whether the app has necessary permissions, asking for them if needed.
+   *
+   * This blocks until permissions are granted/declined, and should not be called on the UI thread.
+   *
+   * Returns true if permissions are granted.
+   */
+  private synchronized boolean checkPermission() {
+    if (ContextCompat.checkSelfPermission(appContext, GET_ACCOUNTS)
+        == PackageManager.PERMISSION_GRANTED) {
+      return true;
+    }
+
+    final Activity activity = activityHolder.get();
+    if (activity == null) {
+      return false;
+    }
+
+    // Check if we have previously been denied permission.
+    if (ActivityCompat.shouldShowRequestPermissionRationale(activity, GET_ACCOUNTS)) {
+      activity.runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          Toast.makeText(activity, R.string.starboard_accounts_permission, Toast.LENGTH_LONG)
+              .show();
+        }
+      });
+      return false;
+    }
+
+    // Request permission.
+    waitingForPermission = true;
+    permissionGranted = false;
+    ActivityCompat.requestPermissions(
+        activity, new String[]{GET_ACCOUNTS}, R.id.rc_get_accounts_permission);
+    try {
+      while (waitingForPermission) {
+        wait();
+      }
+    } catch (InterruptedException e) {
+      return false;
+    }
+    return permissionGranted;
+  }
+
+  /**
+   * Callback pass-thru from the Activity with the result from requesting permissions.
+   */
+  @Override
+  public void onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {
+    if (requestCode == R.id.rc_get_accounts_permission) {
+      synchronized (this) {
+        permissionGranted = grantResults.length > 0
+            && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+        waitingForPermission = false;
+        notifyAll();
+      }
+    }
+  }
+
+  /**
+   * Remember the name of the signed-in account.
+   */
+  private void saveSignedInAccountName(String accountName) {
+    getPreferences().edit().putString(ACCOUNT_NAME_PREF_KEY, accountName).commit();
+  }
+
+  /**
+   * Returns the remembered name of the signed-in account.
+   */
+  private String loadSignedInAccountName() {
+    return getPreferences().getString(ACCOUNT_NAME_PREF_KEY, "");
+  }
+
+  private SharedPreferences getPreferences() {
+    return appContext.getSharedPreferences(SHARED_PREFS_NAME, 0);
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java
new file mode 100644
index 0000000..f8640d8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AudioPermissionRequester.java
@@ -0,0 +1,82 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.UsedByNative;
+
+/** Helper class that requests the record audio permission. */
+public class AudioPermissionRequester {
+  private final Context context;
+  private final Holder<Activity> activityHolder;
+  private long nativePermissionRequestor;
+  // Only use in synchronized methods.
+  private boolean requestAudioPermissionStarted;
+
+  public AudioPermissionRequester(Context context, Holder<Activity> activityHolder) {
+    this.context = context;
+    this.activityHolder = activityHolder;
+  }
+
+  /**
+   * Requests the RECORD_AUDIO permission. Returns true if the permission is granted; returns false
+   * if the permission is not granted yet and starts to request the RECORD_AUDIO permission.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public synchronized boolean requestRecordAudioPermission(long nativePermissionRequestor) {
+    this.nativePermissionRequestor = nativePermissionRequestor;
+    Activity activity = activityHolder.get();
+    if (activity == null) {
+      return false;
+    }
+
+    if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
+        == PackageManager.PERMISSION_GRANTED) {
+      return true;
+    }
+    if (!requestAudioPermissionStarted) {
+      ActivityCompat.requestPermissions(
+          activity, new String[] {Manifest.permission.RECORD_AUDIO}, R.id.rc_record_audio);
+      requestAudioPermissionStarted = true;
+    }
+
+    return false;
+  }
+
+  /** Handles the RECORD_AUDIO request result. */
+  public synchronized void onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {
+    if (requestCode == R.id.rc_record_audio) {
+      // If the request is cancelled, the result arrays are empty.
+      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+        // Permission granted.
+        nativeHandlePermission(nativePermissionRequestor, true);
+      } else {
+        // Permission denied.
+        nativeHandlePermission(nativePermissionRequestor, false);
+      }
+      requestAudioPermissionStarted = false;
+    }
+  }
+
+  private native void nativeHandlePermission(long nativePermissionRequestor, boolean isGranted);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java
new file mode 100644
index 0000000..8c07518
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltA11yHelper.java
@@ -0,0 +1,261 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import dev.cobalt.util.Log;
+import java.util.BitSet;
+import java.util.List;
+
+/**
+ * An ExploreByTouchHelper that create a virtual d-pad grid, so that Cobalt remains functional when
+ * the TalkBack screen reader is enabled (which otherwise intercepts d-pad events for most
+ * applications).
+ */
+class CobaltA11yHelper extends ExploreByTouchHelper {
+  // These are from starboard/key.h
+  private static final int SB_KEY_GAMEPAD_DPAD_UP = 0x800C;
+  private static final int SB_KEY_GAMEPAD_DPAD_DOWN = 0x800D;
+  private static final int SB_KEY_GAMEPAD_DPAD_LEFT = 0x800E;
+  private static final int SB_KEY_GAMEPAD_DPAD_RIGHT = 0x800F;
+
+  // The fake dimensions for the nine virtual views.
+  // These values are arbitrary as long as the views stay on the screen.
+  private static final int FAKE_VIEW_HEIGHT = 10;
+  private static final int FAKE_VIEW_WIDTH = 10;
+
+  private int previousFocusedViewId = 1;
+  // This set tracks whether onPopulateNodeForVirtualView has been
+  // called for each virtual view id.
+  private final BitSet nodePopulatedSet = new BitSet(9);
+  private final Handler handler = new Handler();
+  private boolean hasInitialFocusBeenSet;
+
+  public CobaltA11yHelper(View view) {
+    super(view);
+    ViewCompat.setAccessibilityDelegate(view, this);
+  }
+
+  private static native void nativeInjectKeyEvent(int key);
+
+  @Override
+  protected int getVirtualViewAt(float x, float y) {
+    // This method is only required for touch or mouse interfaces.
+    // Since we don't support either, we simply always return HOST_ID.
+    return HOST_ID;
+  }
+
+  @Override
+  protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+    if (!virtualViewIds.isEmpty()) {
+      throw new RuntimeException("Expected empty list");
+    }
+    // We always have precisely 9 virtual views.
+    for (int i = 1; i <= 9; i++) {
+      virtualViewIds.add(i);
+    }
+  }
+
+  /**
+   * Returns the "patch number" for a given view id, given a focused view id.
+   *
+   * <p>A "patch number" is a 1-9 number that describes where the requestedViewId is now located on
+   * an X-Y grid, given the focusedViewId.
+   *
+   * <p>Patch number grid:
+   * (0,0)----->X
+   *   |+-+-+-+
+   *   ||1|2|3|
+   *   |+-+-+-|
+   *   ||4|5|6|
+   *   |+-+-+-|
+   *   ||7|8|9|
+   *   |+-+-+-+
+   *  \./ Y
+   *
+   * <p>As focus changes, the locations of the views are moved so the focused view is always in the
+   * middle (patch number 5) and all of the other views always in the same relative position with
+   * respect to each other (with those on the edges adjacent to those on the opposite edges --
+   * wrapping around).
+   *
+   * <p>5 is returned whenever focusedViewId = requestedViewId
+   */
+  private static int getPatchNumber(int focusedViewId, int requestedViewId) {
+    // The (x,y) the focused view has in the 9 patch where 5 is in the middle.
+    int focusedX = (focusedViewId - 1) % 3;
+    int focusedY = (focusedViewId - 1) / 3;
+
+    // x and y offsets of focused view where middle is (0, 0)
+    int focusedRelativeToCenterX = focusedX - 1;
+    int focusedRelativeToCenterY = focusedY - 1;
+
+    // The (x,y) the requested view has in the 9 patch where 5 is in the middle.
+    int requestedX = (requestedViewId - 1) % 3;
+    int requestedY = (requestedViewId - 1) / 3;
+
+    // x and y offsets of requested view where middle is (0, 0)
+    int requestedRelativeToCenterX = requestedX - 1;
+    int requestedRelativeToCenterY = requestedY - 1;
+
+    // The (x,y) that the requested view has in the 9 patch when focusedViewId
+    // is in the middle.
+    int translatedRequestedX = (1 + 3 + requestedRelativeToCenterX - focusedRelativeToCenterX) % 3;
+    int translatedRequestedY = (1 + 3 + requestedRelativeToCenterY - focusedRelativeToCenterY) % 3;
+
+    return (translatedRequestedY * 3) + translatedRequestedX + 1;
+  }
+
+  private void maybeInjectEvent(int currentFocusedViewId) {
+    switch (getPatchNumber(previousFocusedViewId, currentFocusedViewId)) {
+      case 5:
+        // no move;
+        break;
+      case 2:
+        nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_UP);
+        break;
+      case 4:
+        nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_LEFT);
+        break;
+      case 6:
+        nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_RIGHT);
+        break;
+      case 8:
+        nativeInjectKeyEvent(SB_KEY_GAMEPAD_DPAD_DOWN);
+        break;
+      default:
+        // TODO: Could support diagonal movements, although it's likely
+        // not possible to reach this.
+        break;
+    }
+    previousFocusedViewId = currentFocusedViewId;
+  }
+
+  @Override
+  protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
+    int focusedViewId = getAccessibilityFocusedVirtualViewId();
+
+    if (focusedViewId < 1 || focusedViewId > 9) {
+      // If this is not one of our nine-patch views, it's probably HOST_ID
+      // In any case, assume there is no focus change.
+      focusedViewId = previousFocusedViewId;
+    }
+
+    // onPopulateNodeForVirtualView() gets called at least once every
+    // time the focused view changes. So see if it's changed since the
+    // last time we've been called and inject an event if so.
+    maybeInjectEvent(focusedViewId);
+
+    int patchNumber = getPatchNumber(focusedViewId, virtualViewId);
+
+    int x = (patchNumber - 1) % 3;
+    int y = (patchNumber - 1) / 3;
+
+    // Note that the specific bounds here are arbitrary. The importance
+    // is the relative bounds to each other.
+    node.setBoundsInParent(new Rect(
+        x * FAKE_VIEW_WIDTH,
+        y * FAKE_VIEW_HEIGHT,
+        x * FAKE_VIEW_WIDTH + FAKE_VIEW_WIDTH,
+        y * FAKE_VIEW_HEIGHT + FAKE_VIEW_HEIGHT));
+    node.setText("");
+
+    if (virtualViewId >= 1 || virtualViewId <= 9) {
+      nodePopulatedSet.set(virtualViewId - 1);
+    }
+    if (!hasInitialFocusBeenSet && nodePopulatedSet.cardinality() == 9) {
+      // Once the ExploreByTouchHelper knows about all of our virtual views,
+      // but not before, ask that the accessibility focus be moved from
+      // it's initial position on HOST_ID to the one we want to start with.
+      hasInitialFocusBeenSet = true;
+      handler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              sendEventForVirtualView(
+                  previousFocusedViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+            }
+          });
+    }
+  }
+
+  @Override
+  protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
+    return false;
+  }
+
+  /** A simple equivilent to Assert.assertEquals so we don't depend on junit */
+  private static void assertEquals(int expected, int actual) {
+    if (expected != actual) {
+      throw new RuntimeException("Expected " + expected + " actual " + actual);
+    }
+  }
+
+  /**
+   * Unit test for getPatchNumber().
+   *
+   * <p>As of this writing, the Java portion of the Cobalt build has no unit test mechanism.
+   *
+   * <p>To run this test, simply call it from application start and start the application.
+   *
+   * <p>TODO: Move this to a real unit test location when one exists.
+   */
+  private static void testGetPatchNumber() {
+    Log.i(TAG, "+testGetPatchNumber");
+
+    assertEquals(1, getPatchNumber(5, 1));
+    assertEquals(2, getPatchNumber(5, 2));
+    assertEquals(3, getPatchNumber(5, 3));
+    assertEquals(4, getPatchNumber(5, 4));
+    assertEquals(5, getPatchNumber(5, 5));
+    assertEquals(6, getPatchNumber(5, 6));
+    assertEquals(7, getPatchNumber(5, 7));
+    assertEquals(8, getPatchNumber(5, 8));
+    assertEquals(9, getPatchNumber(5, 9));
+
+    for (int i = 1; i <= 9; i++) {
+      assertEquals(5, getPatchNumber(i, i));
+    }
+
+    assertEquals(5, getPatchNumber(1, 1));
+    assertEquals(6, getPatchNumber(1, 2));
+    assertEquals(4, getPatchNumber(1, 3));
+    assertEquals(8, getPatchNumber(1, 4));
+    assertEquals(9, getPatchNumber(1, 5));
+    assertEquals(7, getPatchNumber(1, 6));
+    assertEquals(2, getPatchNumber(1, 7));
+    assertEquals(3, getPatchNumber(1, 8));
+    assertEquals(1, getPatchNumber(1, 9));
+
+    assertEquals(9, getPatchNumber(9, 1));
+    assertEquals(7, getPatchNumber(9, 2));
+    assertEquals(8, getPatchNumber(9, 3));
+    assertEquals(3, getPatchNumber(9, 4));
+    assertEquals(1, getPatchNumber(9, 5));
+    assertEquals(2, getPatchNumber(9, 6));
+    assertEquals(6, getPatchNumber(9, 7));
+    assertEquals(4, getPatchNumber(9, 8));
+    assertEquals(5, getPatchNumber(9, 9));
+    Log.i(TAG, "-testGetPatchNumber");
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
new file mode 100644
index 0000000..2c68ab9
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
@@ -0,0 +1,261 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.app.NativeActivity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import dev.cobalt.media.VideoSurfaceView;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Native activity that has the required JNI methods called by the Starboard implementation. */
+public abstract class CobaltActivity extends NativeActivity {
+
+  // A place to put args while debugging so they're used even when starting from the launcher.
+  // This should always be empty in submitted code.
+  private static final String[] DEBUG_ARGS = {};
+
+  private static final String URL_ARG = "--url=";
+  private static final java.lang.String META_DATA_APP_URL = "cobalt.APP_URL";
+
+  private static final String SPLASH_URL_ARG = "--fallback_splash_screen_url=";
+  private static final java.lang.String META_DATA_SPLASH_URL = "cobalt.SPLASH_URL";
+
+  private static final String FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
+      "--force_migration_for_storage_partitioning";
+  private static final String META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
+      "cobalt.force_migration_for_storage_partitioning";
+
+  @SuppressWarnings("unused")
+  private CobaltA11yHelper a11yHelper;
+
+  private VideoSurfaceView videoSurfaceView;
+
+  private ViewTreeObserver.OnGlobalLayoutListener videoSurfaceLayoutListener;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    // To ensure that volume controls adjust the correct stream, make this call
+    // early in the app's lifecycle. This connects the volume controls to
+    // STREAM_MUSIC whenever the target activity or fragment is visible.
+    setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+    String startDeepLink = getIntentUrlAsString(getIntent());
+    if (getStarboardBridge() == null) {
+      // Cold start - Instantiate the singleton StarboardBridge.
+      StarboardBridge starboardBridge = createStarboardBridge(getArgs(), startDeepLink);
+      ((StarboardBridge.HostApplication) getApplication()).setStarboardBridge(starboardBridge);
+    } else {
+      // Warm start - Pass the deep link to the running Starboard app.
+      getStarboardBridge().handleDeepLink(startDeepLink);
+    }
+
+    // super.onCreate() will cause an APP_CMD_START in native code,
+    // so make sure to initialize any state beforehand that might be touched by
+    // native code invocations.
+    super.onCreate(savedInstanceState);
+
+    videoSurfaceView = new VideoSurfaceView(this);
+    a11yHelper = new CobaltA11yHelper(videoSurfaceView);
+    addContentView(
+        videoSurfaceView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+    videoSurfaceLayoutListener =
+        new ViewTreeObserver.OnGlobalLayoutListener() {
+          @Override
+          public void onGlobalLayout() {
+            VideoSurfaceView.nativeOnGlobalLayout();
+          }
+        };
+    ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
+    observer.addOnGlobalLayoutListener(videoSurfaceLayoutListener);
+  }
+
+  /**
+   * Instantiates the StarboardBridge. Apps not supporting sign-in should inject an instance of
+   * NoopUserAuthorizer. Apps may subclass StarboardBridge if they need to override anything.
+   */
+  protected abstract StarboardBridge createStarboardBridge(String[] args, String startDeepLink);
+
+  @UsedByNative
+  protected StarboardBridge getStarboardBridge() {
+    return ((StarboardBridge.HostApplication) getApplication()).getStarboardBridge();
+  }
+
+  @Override
+  protected void onStart() {
+    getStarboardBridge().onActivityStart(this);
+    super.onStart();
+  }
+
+  @Override
+  protected void onStop() {
+    getStarboardBridge().onActivityStop(this);
+    super.onStop();
+  }
+
+  @Override
+  protected void onDestroy() {
+    ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
+    observer.removeOnGlobalLayoutListener(videoSurfaceLayoutListener);
+
+    super.onDestroy();
+    getStarboardBridge().onActivityDestroy(this);
+  }
+
+  @Override
+  public boolean onSearchRequested() {
+    return getStarboardBridge().onSearchRequested();
+  }
+
+  /** Returns true if the argument list contains an arg starting with argName. */
+  private static boolean hasArg(List<String> args, String argName) {
+    for (String arg : args) {
+      if (arg.startsWith(argName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Get argv/argc style args, if any from intent extras. Returns empty array if there are none
+   *
+   * <p>To use, invoke application via, eg, adb shell am start --esa args arg1,arg2 \
+   * dev.cobalt.coat/dev.cobalt.app.MainActivity
+   */
+  protected String[] getArgs() {
+    Bundle extras = getIntent().getExtras();
+    CharSequence[] argsExtra =
+        (extras == null || isReleaseBuild()) ? null : extras.getCharSequenceArray("args");
+
+    List<String> args = new ArrayList<>(Arrays.asList(DEBUG_ARGS));
+    if (argsExtra != null) {
+      for (int i = 0; i < argsExtra.length; i++) {
+        args.add(argsExtra[i].toString());
+      }
+    }
+
+    // If the URL arg isn't specified, get it from AndroidManifest.xml.
+    boolean hasUrlArg = hasArg(args, URL_ARG);
+    // If the splash screen url arg isn't specified, get it from AndroidManifest.xml.
+    boolean hasSplashUrlArg = hasArg(args, SPLASH_URL_ARG);
+    if (!hasUrlArg || !hasSplashUrlArg) {
+      try {
+        ActivityInfo ai =
+            getPackageManager()
+                .getActivityInfo(getIntent().getComponent(), PackageManager.GET_META_DATA);
+        if (ai.metaData != null) {
+          if (!hasUrlArg) {
+            String url = ai.metaData.getString(META_DATA_APP_URL);
+            if (url != null) {
+              args.add(URL_ARG + url);
+            }
+          }
+          if (!hasSplashUrlArg) {
+            String splashUrl = ai.metaData.getString(META_DATA_SPLASH_URL);
+            if (splashUrl != null) {
+              args.add(SPLASH_URL_ARG + splashUrl);
+            }
+          }
+          if (ai.metaData.getBoolean(META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING)) {
+            args.add(FORCE_MIGRATION_FOR_STORAGE_PARTITIONING);
+          }
+        }
+      } catch (NameNotFoundException e) {
+        throw new RuntimeException("Error getting activity info", e);
+      }
+    }
+
+    return args.toArray(new String[0]);
+  }
+
+  protected boolean isReleaseBuild() {
+    return StarboardBridge.isReleaseBuild();
+  }
+
+  @Override
+  protected void onNewIntent(Intent intent) {
+    getStarboardBridge().handleDeepLink(getIntentUrlAsString(intent));
+  }
+
+  /**
+   * Returns the URL from an Intent as a string. This may be overridden for additional processing.
+   */
+  protected String getIntentUrlAsString(Intent intent) {
+    Uri intentUri = intent.getData();
+    return (intentUri == null) ? null : intentUri.toString();
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    super.onActivityResult(requestCode, resultCode, data);
+    getStarboardBridge().onActivityResult(requestCode, resultCode, data);
+  }
+
+  @Override
+  public void onRequestPermissionsResult(
+      int requestCode, String[] permissions, int[] grantResults) {
+    getStarboardBridge().onRequestPermissionsResult(requestCode, permissions, grantResults);
+  }
+
+  public void setVideoSurfaceBounds(final int x, final int y, final int width, final int height) {
+    if (!videoSurfaceView.updateVideoBounds(x, y, width, height)) {
+      return;
+    }
+
+    VideoSurfaceView.nativeOnLayoutNeeded();
+    runOnUiThread(
+        new Runnable() {
+          @Override
+          public void run() {
+            VideoSurfaceView.nativeOnLayoutScheduled();
+            LayoutParams layoutParams = videoSurfaceView.getLayoutParams();
+            // Since videoSurfaceView is added directly to the Activity's content view, which is a
+            // FrameLayout, we expect its layout params to become FrameLayout.LayoutParams.
+            if (layoutParams instanceof FrameLayout.LayoutParams) {
+              ((FrameLayout.LayoutParams) layoutParams).setMargins(x, y, x + width, y + height);
+            } else {
+              Log.w(
+                  TAG,
+                  "Unexpected video surface layout params class "
+                      + layoutParams.getClass().getName());
+            }
+            layoutParams.width = width;
+            layoutParams.height = height;
+            // Even though as a NativeActivity we're not using the Android UI framework, by setting
+            // the  layout params it will force a layout to be requested. That will cause the
+            // SurfaceView to position its underlying Surface to match the screen coordinates of
+            // where the view would be in a UI layout and to set the surface transform matrix to
+            // match the view's size.
+            videoSurfaceView.setLayoutParams(layoutParams);
+          }
+        });
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java
new file mode 100644
index 0000000..56a2ffd
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java
@@ -0,0 +1,103 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import dev.cobalt.util.Log;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/** Helper class that implements an HTTP POST function used by DRM one-time provisioning. */
+public class CobaltHttpHelper {
+  private static final int RETRY_SLEEP_MILLIS = 250;
+  private static final int MAX_ATTEMPTS = 3;
+
+  /** Exception representing a transient HTTP failure (eg, 500). */
+  private static class TransientFailure extends Exception {}
+
+  private static void sleepBeforeRetry() {
+    try {
+      Thread.sleep(RETRY_SLEEP_MILLIS);
+    } catch (InterruptedException ex) {
+      // should never happen
+    }
+  }
+
+  /**
+   * Performs an HTTP POST, sending postData to url and returning the response contents on 200.
+   *
+   * <p>Note that this function retries temporary failures (network, HTTP 500) a few times.
+   *
+   * <p>Note also that this sets a few DRM-specific headers.
+   *
+   * @return response contents on success, null for permanent failure or exception.
+   */
+  public byte[] performDrmHttpPost(String url) {
+    for (int attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
+      try {
+        return internalPerformHttpPost(url);
+      } catch (IOException ex) {
+        Log.w(TAG, "performHttpPost IOException: ", ex);
+        // continue below
+      } catch (TransientFailure ex) {
+        // continue below
+      } catch (Throwable tr) {
+        // All other exceptions are caught because the caller is expected
+        // to be a JNI function, where exception handling is inconvenient.
+        Log.e(TAG, "performHttpPost exception: ", tr);
+        return null;
+      }
+      sleepBeforeRetry();
+    }
+    Log.w(TAG, "performHttpPost: Max attempts attempted");
+    return null;
+  }
+
+  private byte[] internalPerformHttpPost(String url) throws IOException, TransientFailure {
+    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+    try {
+      conn.setRequestMethod("POST");
+      conn.setDoOutput(false);
+      conn.setDoInput(true);
+
+      int statusCode = conn.getResponseCode();
+
+      if (statusCode >= 500 && statusCode <= 599) {
+        // We retry on 5xx failures.
+        Log.i(TAG, "performHttpPost transient failure: " + conn.getResponseMessage());
+        throw new TransientFailure();
+      }
+
+      if (statusCode != 200) {
+        Log.i(TAG, "performHttpPost permanent failure: " + conn.getResponseMessage());
+        return null;
+      }
+
+      ByteArrayOutputStream output = new ByteArrayOutputStream();
+      BufferedInputStream input = new BufferedInputStream(conn.getInputStream());
+      for (int b = input.read(); b != -1; b = input.read()) {
+        output.write(b);
+      }
+
+      return output.toByteArray();
+    } finally {
+      conn.disconnect();
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java
new file mode 100644
index 0000000..f7809b0
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltTextToSpeechHelper.java
@@ -0,0 +1,206 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.speech.tts.TextToSpeech;
+import android.view.accessibility.AccessibilityManager;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to implement the SbSpeechSynthesis* Starboard API for Audio accessibility.
+ *
+ * <p>This class is intended to be a singleton in the system. It creates a single static Handler
+ * thread in lieu of other synchronization options.
+ */
+class CobaltTextToSpeechHelper
+    implements TextToSpeech.OnInitListener,
+        AccessibilityManager.AccessibilityStateChangeListener,
+        AccessibilityManager.TouchExplorationStateChangeListener {
+  private final Context context;
+  private final Runnable stopRequester;
+  private final HandlerThread thread;
+  private final Handler handler;
+
+  // The TTS engine should be used only on the background thread.
+  private TextToSpeech ttsEngine;
+
+  private boolean wasScreenReaderEnabled;
+
+  private enum State {
+    PENDING,
+    INITIALIZED,
+    FAILED
+  }
+
+  // These are only accessed inside the Handler Thread
+  private State state = State.PENDING;
+  private long nextUtteranceId;
+  private final List<String> pendingUtterances = new ArrayList<>();
+
+  CobaltTextToSpeechHelper(Context context, Runnable stopRequester) {
+    this.context = context;
+    this.stopRequester = stopRequester;
+
+    thread = new HandlerThread("CobaltTextToSpeechHelper");
+    thread.start();
+    handler = new Handler(thread.getLooper());
+
+    AccessibilityManager accessibilityManager =
+        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+    wasScreenReaderEnabled = isScreenReaderEnabled();
+    accessibilityManager.addAccessibilityStateChangeListener(this);
+    accessibilityManager.addTouchExplorationStateChangeListener(this);
+  }
+
+  public void shutdown() {
+
+    handler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (ttsEngine != null) {
+              ttsEngine.shutdown();
+            }
+          }
+        });
+    thread.quitSafely();
+
+    AccessibilityManager accessibilityManager =
+        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+    accessibilityManager.removeAccessibilityStateChangeListener(this);
+    accessibilityManager.removeTouchExplorationStateChangeListener(this);
+  }
+
+  /** Returns whether a screen reader is currently enabled */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean isScreenReaderEnabled() {
+    AccessibilityManager am =
+        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+    final List<AccessibilityServiceInfo> screenReaders =
+        am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
+    return !screenReaders.isEmpty();
+  }
+
+  /** Implementation of TextToSpeech.OnInitListener */
+  @Override
+  public void onInit(final int status) {
+    handler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (status != TextToSpeech.SUCCESS) {
+              Log.e(TAG, "TextToSpeech.onInit failure: " + status);
+              state = State.FAILED;
+              return;
+            }
+            state = State.INITIALIZED;
+            for (String utterance : pendingUtterances) {
+              speak(utterance);
+            }
+            pendingUtterances.clear();
+          }
+        });
+  }
+
+  /**
+   * Speaks the given text, enqueuing it if something is already speaking. Java-layer implementation
+   * of Starboard's SbSpeechSynthesisSpeak.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void speak(final String text) {
+
+    handler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+
+            if (ttsEngine == null) {
+              ttsEngine = new TextToSpeech(context, CobaltTextToSpeechHelper.this);
+            }
+
+            switch (state) {
+              case PENDING:
+                pendingUtterances.add(text);
+                break;
+              case INITIALIZED:
+                int success =
+                    ttsEngine.speak(
+                        text, TextToSpeech.QUEUE_ADD, null, Long.toString(nextUtteranceId++));
+
+                if (success != TextToSpeech.SUCCESS) {
+                  Log.e(TAG, "TextToSpeech.speak error: " + success);
+                  return;
+                }
+                break;
+              case FAILED:
+                break;
+            }
+          }
+        });
+  }
+
+  /** Cancels all speaking. Java-layer implementation of Starboard's SbSpeechSynthesisCancel. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void cancel() {
+    handler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (ttsEngine != null) {
+              ttsEngine.stop();
+            }
+            pendingUtterances.clear();
+          }
+        });
+  }
+
+  @Override
+  public void onAccessibilityStateChanged(boolean enabled) {
+    // Note that this callback isn't perfect since it only tells us if accessibility was entirely
+    // enabled/disabled, but it's better than nothing. For example, it won't be called if the screen
+    // reader is enabled/disabled while text magnification remains enabled.
+    finishIfScreenReaderChanged();
+  }
+
+  @Override
+  public void onTouchExplorationStateChanged(boolean enabled) {
+    // We also listen for talkback changes because it's the standard (but not only) screen reader,
+    // and we can get a better signal than just listening for accessibility being enabled/disabled.
+    finishIfScreenReaderChanged();
+  }
+
+  /**
+   * Quit the app if screen reader settings changed so we respect the new setting the next time the
+   * app is run. This should only happen while stopped in the background since the user has to leave
+   * the app to change the setting.
+   */
+  private void finishIfScreenReaderChanged() {
+    if (wasScreenReaderEnabled != isScreenReaderEnabled()) {
+      stopRequester.run();
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java
new file mode 100644
index 0000000..35bc1f0
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/ErrorDialog.java
@@ -0,0 +1,125 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * A fullscreen dialog to show an error, with up to 3 buttons. This has a look similar to the
+ * Android TV leanback ErrorFragment. As a dialog, it creates its own window so it can be shown on
+ * top of our NativeActivity, unlike a fragment.
+ */
+class ErrorDialog extends Dialog {
+
+  public static final int MAX_BUTTONS = 3;
+
+  private final Params params;
+
+  private static class Params {
+    private int messageId;
+    private int numButtons = 0;
+    private int[] buttonIds = new int[MAX_BUTTONS];
+    private int[] buttonLabelIds = new int[MAX_BUTTONS];
+    private OnClickListener buttonClickListener;
+    private OnDismissListener dismissListener;
+  }
+
+  public static class Builder {
+
+    private Context context;
+    private Params params = new Params();
+
+    public Builder(Context context) {
+      this.context = context;
+    }
+
+    public Builder setMessage(int messageId) {
+      params.messageId = messageId;
+      return this;
+    }
+
+    public Builder addButton(int id, int labelId) {
+      if (params.numButtons >= MAX_BUTTONS) {
+        throw new IllegalArgumentException("Too many buttons");
+      }
+      params.buttonIds[params.numButtons] = id;
+      params.buttonLabelIds[params.numButtons] = labelId;
+      params.numButtons++;
+      return this;
+    }
+
+    public Builder setButtonClickListener(OnClickListener buttonClickListener) {
+      params.buttonClickListener = buttonClickListener;
+      return this;
+    }
+
+    public Builder setOnDismissListener(OnDismissListener dismissListener) {
+      params.dismissListener = dismissListener;
+      return this;
+    }
+
+    public ErrorDialog create() {
+      return new ErrorDialog(context, params);
+    }
+  }
+
+  private ErrorDialog(Context context, Params params) {
+    super(context);
+    this.params = params;
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.coat_error_dialog);
+
+    ImageView imageView = (ImageView) findViewById(R.id.image);
+    Drawable drawable = getContext().getResources().getDrawable(
+        R.drawable.lb_ic_sad_cloud, getContext().getTheme());
+    imageView.setImageDrawable(drawable);
+
+    TextView messageView = (TextView) findViewById(R.id.message);
+    messageView.setText(params.messageId);
+
+    Button button = (Button) findViewById(R.id.button_1);
+    ViewGroup container = (ViewGroup) button.getParent();
+    int buttonIndex = container.indexOfChild(button);
+
+    for (int i = 0; i < params.numButtons; i++) {
+      button = (Button) container.getChildAt(buttonIndex + i);
+      button.setText(params.buttonLabelIds[i]);
+      button.setVisibility(View.VISIBLE);
+
+      final int buttonId = params.buttonIds[i];
+      button.setOnClickListener(new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+          params.buttonClickListener.onClick(ErrorDialog.this, buttonId);
+        }
+      });
+    }
+
+    setOnDismissListener(params.dismissListener);
+  }
+
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java
new file mode 100644
index 0000000..4aa82e7
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/PlatformError.java
@@ -0,0 +1,153 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.annotation.IntDef;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Shows an ErrorDialog to inform the user of a Starboard platform error.
+ */
+public class PlatformError
+    implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({CONNECTION_ERROR})
+  @interface ErrorType {}
+  // This must be kept in sync with starboard/android/shared/system_platform_error.cc
+  public static final int CONNECTION_ERROR = 0;
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({CANCELLED, NEGATIVE, POSITIVE})
+  @interface Response {}
+  public static final int NEGATIVE = -1;
+  public static final int CANCELLED = 0;
+  public static final int POSITIVE = 1;
+
+  // Button IDs for CONNECTION_ERROR
+  private static final int RETRY_BUTTON = 1;
+  private static final int NETWORK_SETTINGS_BUTTON = 2;
+
+  private final Holder<Activity> activityHolder;
+  private final @ErrorType int errorType;
+  private final long data;
+  private final Handler uiThreadHandler;
+
+  private Dialog dialog;
+  private int response;
+
+  public PlatformError(Holder<Activity> activityHolder, @ErrorType int errorType, long data) {
+    this.activityHolder = activityHolder;
+    this.errorType = errorType;
+    this.data = data;
+    uiThreadHandler = new Handler(Looper.getMainLooper());
+    response = CANCELLED;
+  }
+
+  /** Display the error. */
+  public void raise() {
+    uiThreadHandler.post(new Runnable() {
+      @Override
+      public void run() {
+        showDialogOnUiThread();
+      }
+    });
+  }
+
+  private void showDialogOnUiThread() {
+    Activity activity = activityHolder.get();
+    if (activity == null) {
+      onCleared(CANCELLED, data);
+      return;
+    }
+    ErrorDialog.Builder dialogBuilder = new ErrorDialog.Builder(activity);
+    switch (errorType) {
+      case CONNECTION_ERROR:
+        dialogBuilder
+            .setMessage(R.string.starboard_platform_connection_error)
+            .addButton(RETRY_BUTTON, R.string.starboard_platform_retry)
+            .addButton(NETWORK_SETTINGS_BUTTON, R.string.starboard_platform_network_settings);
+        break;
+      default:
+        Log.e(TAG, "Unknown platform error " + errorType);
+        return;
+    }
+    dialog = dialogBuilder
+        .setButtonClickListener(this)
+        .setOnDismissListener(this)
+        .create();
+    dialog.show();
+  }
+
+  /** Programmatically dismiss the error. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void clear() {
+    uiThreadHandler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (dialog != null) {
+              dialog.dismiss();
+            }
+          }
+        });
+  }
+
+  @Override
+  public void onClick(DialogInterface dialogInterface, int whichButton) {
+    if (errorType == CONNECTION_ERROR) {
+      switch (whichButton) {
+        case NETWORK_SETTINGS_BUTTON:
+          Activity activity = activityHolder.get();
+          if (activity != null) {
+            activity.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
+          }
+          break;
+        case RETRY_BUTTON:
+          response = POSITIVE;
+          dialog.dismiss();
+          break;
+        default: // fall out
+      }
+    }
+  }
+
+  @Override
+  public void onDismiss(DialogInterface dialogInterface) {
+    dialog = null;
+    onCleared(response, data);
+  }
+
+  /** Informs Starboard when the error is dismissed. */
+  protected void onCleared(@PlatformError.Response int response, long data) {
+    nativeOnCleared(response, data);
+  }
+
+  private native void nativeOnCleared(@PlatformError.Response int response, long data);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
new file mode 100644
index 0000000..7fb75e5
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
@@ -0,0 +1,567 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import static android.content.Context.AUDIO_SERVICE;
+import static android.media.AudioManager.GET_DEVICES_INPUTS;
+import static dev.cobalt.util.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.util.Pair;
+import android.util.Size;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.CaptioningManager;
+import dev.cobalt.account.UserAuthorizer;
+import dev.cobalt.feedback.FeedbackService;
+import dev.cobalt.media.AudioOutputManager;
+import dev.cobalt.media.CaptionSettings;
+import dev.cobalt.media.CobaltMediaSession;
+import dev.cobalt.media.MediaImage;
+import dev.cobalt.util.DisplayUtil;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.lang.reflect.Method;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+
+/** Implementation of the required JNI methods called by the Starboard C++ code. */
+public class StarboardBridge {
+
+  /** Interface to be implemented by the Android Application hosting the starboard app. */
+  public interface HostApplication {
+    void setStarboardBridge(StarboardBridge starboardBridge);
+
+    StarboardBridge getStarboardBridge();
+  }
+
+  private CobaltTextToSpeechHelper ttsHelper;
+  private UserAuthorizer userAuthorizer;
+  private FeedbackService feedbackService;
+  private AudioOutputManager audioOutputManager;
+  private CobaltMediaSession cobaltMediaSession;
+  private VoiceRecognizer voiceRecognizer;
+  private AudioPermissionRequester audioPermissionRequester;
+
+  static {
+    // Even though NativeActivity already loads our library from C++,
+    // we still have to load it from Java to make JNI calls into it.
+    System.loadLibrary("coat");
+  }
+
+  private final Context appContext;
+  private final Holder<Activity> activityHolder;
+  private final String[] args;
+  private final String startDeepLink;
+  private final Runnable stopRequester =
+      new Runnable() {
+        @Override
+        public void run() {
+          requestStop(0);
+        }
+      };
+
+  private volatile boolean starboardStopped = false;
+
+  public StarboardBridge(
+      Context appContext,
+      Holder<Activity> activityHolder,
+      UserAuthorizer userAuthorizer,
+      FeedbackService feedbackService,
+      String[] args,
+      String startDeepLink) {
+
+    // Make sure the JNI stack is properly initialized first as there is
+    // race condition as soon as any of the following objects creates a new thread.
+    nativeInitialize();
+
+    this.appContext = appContext;
+    this.activityHolder = activityHolder;
+    this.args = args;
+    this.startDeepLink = startDeepLink;
+    this.ttsHelper = new CobaltTextToSpeechHelper(appContext, stopRequester);
+    this.userAuthorizer = userAuthorizer;
+    this.feedbackService = feedbackService;
+    this.audioOutputManager = new AudioOutputManager(appContext);
+    this.cobaltMediaSession =
+        new CobaltMediaSession(appContext, activityHolder, audioOutputManager);
+    this.audioPermissionRequester = new AudioPermissionRequester(appContext, activityHolder);
+    this.voiceRecognizer =
+        new VoiceRecognizer(appContext, activityHolder, audioPermissionRequester);
+  }
+
+  private native boolean nativeInitialize();
+
+  protected void onActivityStart(Activity activity) {
+    activityHolder.set(activity);
+  }
+
+  protected void onActivityStop(Activity activity) {
+    if (activityHolder.get() == activity) {
+      activityHolder.set(null);
+    }
+  }
+
+  protected void onActivityDestroy(Activity activity) {
+    if (starboardStopped) {
+      // We can't restart the starboard app, so kill the process for a clean start next time.
+      Log.i(TAG, "Activity destroyed after shutdown; killing app.");
+      System.exit(0);
+    } else {
+      Log.i(TAG, "Activity destroyed without shutdown; app suspended in background.");
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void beforeStartOrResume() {
+    Log.i(TAG, "Prepare to resume");
+    // Bring our platform services to life before resuming so that they're ready to deal with
+    // whatever the web app wants to do with them as part of its start/resume logic.
+    cobaltMediaSession.resume();
+    feedbackService.connect();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void beforeSuspend() {
+    Log.i(TAG, "Prepare to suspend");
+    // We want the MediaSession to be deactivated immediately before suspending so that by the time
+    // the launcher is visible our "Now Playing" card is already gone. Then Cobalt and the web app
+    // can take their time suspending after that.
+    cobaltMediaSession.suspend();
+    feedbackService.disconnect();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void afterStopped() {
+    starboardStopped = true;
+    ttsHelper.shutdown();
+    userAuthorizer.shutdown();
+    Activity activity = activityHolder.get();
+    if (activity != null) {
+      // Wait until the activity is destroyed to exit.
+      Log.i(TAG, "Shutdown in foreground; finishing Activity and removing task.");
+      activity.finishAndRemoveTask();
+    } else {
+      // We can't restart the starboard app, so kill the process for a clean start next time.
+      Log.i(TAG, "Shutdown in background; killing app without removing task.");
+      System.exit(0);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void requestStop(int errorLevel) {
+    if (!starboardStopped) {
+      nativeStopApp(errorLevel);
+    }
+  }
+
+  private native void nativeStopApp(int errorLevel);
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void requestSuspend() {
+    Activity activity = activityHolder.get();
+    if (activity != null) {
+      activity.finish();
+    }
+  }
+
+  public boolean onSearchRequested() {
+    return nativeOnSearchRequested();
+  }
+
+  private native boolean nativeOnSearchRequested();
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public Context getApplicationContext() {
+    return appContext;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  PlatformError raisePlatformError(@PlatformError.ErrorType int errorType, long data) {
+    PlatformError error = new PlatformError(activityHolder, errorType, data);
+    error.raise();
+    return error;
+  }
+
+  /** Returns true if the native code is compiled for release (i.e. 'gold' build). */
+  public static boolean isReleaseBuild() {
+    return nativeIsReleaseBuild();
+  }
+
+  private static native boolean nativeIsReleaseBuild();
+
+  protected Context getAppContext() {
+    return appContext;
+  }
+
+  protected Holder<Activity> getActivityHolder() {
+    return activityHolder;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  protected String[] getArgs() {
+    return args;
+  }
+
+  /** Returns the URL from the Intent that started the app. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  protected String getStartDeepLink() {
+    return startDeepLink;
+  }
+
+  /** Sends an event to the web app to navigate to the given URL */
+  public void handleDeepLink(String url) {
+    nativeHandleDeepLink(url);
+  }
+
+  private native void nativeHandleDeepLink(String url);
+
+  /**
+   * Returns the absolute path to the directory where application specific files should be written.
+   * May be overridden for use cases that need to segregate storage.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  protected String getFilesAbsolutePath() {
+    return appContext.getFilesDir().getAbsolutePath();
+  }
+
+  /**
+   * Returns the absolute path to the application specific cache directory on the filesystem. May be
+   * overridden for use cases that need to segregate storage.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  protected String getCacheAbsolutePath() {
+    return appContext.getCacheDir().getAbsolutePath();
+  }
+
+  /**
+   * Returns non-loopback network interface address and its netmask, or null if none.
+   *
+   * <p>A Java function to help implement Starboard's SbSocketGetLocalInterfaceAddress.
+   *
+   * <p>Required for platforms older than 24. Since 24, bionic includes getifaddrs() which can be
+   * used by the C layer directly.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  Pair<byte[], byte[]> getLocalInterfaceAddressAndNetask(boolean wantIPv6) {
+    try {
+      Enumeration<NetworkInterface> it = NetworkInterface.getNetworkInterfaces();
+
+      while (it.hasMoreElements()) {
+        NetworkInterface ni = it.nextElement();
+        if (ni.isLoopback()) {
+          continue;
+        }
+        if (!ni.isUp()) {
+          continue;
+        }
+        if (ni.isPointToPoint()) {
+          continue;
+        }
+
+        for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
+          byte[] address = ia.getAddress().getAddress();
+          boolean isIPv6 = (address.length > 4);
+          if (isIPv6 == wantIPv6) {
+            // Convert the network prefix length to a network mask.
+            int prefix = ia.getNetworkPrefixLength();
+            byte[] netmask = new byte[address.length];
+            for (int i = 0; i < netmask.length; i++) {
+              if (prefix == 0) {
+                netmask[i] = 0;
+              } else if (prefix >= 8) {
+                netmask[i] = (byte) 0xFF;
+                prefix -= 8;
+              } else {
+                netmask[i] = (byte) (0xFF << (8 - prefix));
+                prefix = 0;
+              }
+            }
+            return new Pair<>(address, netmask);
+          }
+        }
+      }
+    } catch (SocketException ex) {
+      // TODO should we have a logging story that strips logs for production?
+      Log.w(TAG, "sbSocketGetLocalInterfaceAddress exception", ex);
+      return null;
+    }
+    return null;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  CobaltTextToSpeechHelper getTextToSpeechHelper() {
+    return ttsHelper;
+  }
+
+  /** @return A new CaptionSettings object with the current system caption settings. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  CaptionSettings getCaptionSettings() {
+    CaptioningManager cm =
+        (CaptioningManager) appContext.getSystemService(Context.CAPTIONING_SERVICE);
+    return new CaptionSettings(cm);
+  }
+
+  /** Java-layer implementation of SbSystemGetLocaleId. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  String systemGetLocaleId() {
+    return Locale.getDefault().toLanguageTag();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  Size getDisplaySize() {
+    return DisplayUtil.getSystemDisplaySize(appContext);
+  }
+
+  /**
+   * Checks if there is no microphone connected to the system.
+   *
+   * @return true if no device is connected.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean isMicrophoneDisconnected() {
+    if (Build.VERSION.SDK_INT >= 23) {
+      return !isMicrophoneConnectedV23();
+    } else {
+      // There is no way of checking for a connected microphone/device before API 23, so cannot
+      // guarantee that no microphone is connected.
+      return false;
+    }
+  }
+
+  @TargetApi(23)
+  private boolean isMicrophoneConnectedV23() {
+    // A check specifically for microphones is not available before API 28, so it is assumed that a
+    // connected input audio device is a microphone.
+    AudioManager audioManager = (AudioManager) appContext.getSystemService(AUDIO_SERVICE);
+    AudioDeviceInfo[] devices = audioManager.getDevices(GET_DEVICES_INPUTS);
+    return devices.length > 0;
+  }
+
+  /**
+   * Checks if the microphone is muted.
+   *
+   * @return true if the microphone mute is on.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean isMicrophoneMute() {
+    AudioManager audioManager = (AudioManager) appContext.getSystemService(AUDIO_SERVICE);
+    return audioManager.isMicrophoneMute();
+  }
+
+  /** @return true if we have an active network connection and it's on a wireless network. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  boolean isCurrentNetworkWireless() {
+    ConnectivityManager connMgr =
+        (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+    NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
+
+    if (activeInfo == null) {
+      return false;
+    }
+
+    switch (activeInfo.getType()) {
+      case ConnectivityManager.TYPE_ETHERNET:
+        return false;
+      default:
+        // Consider anything that's not definitely wired to be wireless.
+        // For example, TYPE_VPN is ambiguous, but it's highly likely to be
+        // over wifi.
+        return true;
+    }
+  }
+
+  /**
+   * @return true if the user has enabled accessibility high contrast text in the operating system.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  boolean isAccessibilityHighContrastTextEnabled() {
+    AccessibilityManager am =
+        (AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+    try {
+      Method m = AccessibilityManager.class.getDeclaredMethod("isHighTextContrastEnabled");
+
+      return m.invoke(am) == Boolean.TRUE;
+    } catch (ReflectiveOperationException ex) {
+      return false;
+    }
+  }
+
+  /** Returns Java layer implementation for AndroidUserAuthorizer */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  UserAuthorizer getUserAuthorizer() {
+    return userAuthorizer;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void sendFeedback(
+      HashMap<String, String> productSpecificData, String categoryTag, byte[] screenshotData) {
+    // Convert the screenshot byte array into a Bitmap.
+    Bitmap screenshotBitmap = null;
+    if ((screenshotData != null) && (screenshotData.length > 0)) {
+      screenshotBitmap = BitmapFactory.decodeByteArray(screenshotData, 0, screenshotData.length);
+      if (screenshotBitmap == null) {
+        Log.e(TAG, "Unable to decode a screenshot from the data.");
+      }
+    }
+    feedbackService.sendFeedback(productSpecificData, categoryTag, screenshotBitmap);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void updateMediaSession(
+      int playbackState,
+      long actions,
+      long positionMs,
+      float speed,
+      String title,
+      String artist,
+      String album,
+      MediaImage[] artwork) {
+    cobaltMediaSession.updateMediaSession(
+        playbackState, actions, positionMs, speed, title, artist, album, artwork);
+  }
+
+  /** Returns string for kSbSystemPropertyUserAgentAuxField */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  String getUserAgentAuxField() {
+    StringBuilder sb = new StringBuilder();
+
+    String packageName = appContext.getApplicationInfo().packageName;
+    sb.append(packageName);
+    sb.append('/');
+
+    try {
+      sb.append(appContext.getPackageManager().getPackageInfo(packageName, 0).versionName);
+    } catch (PackageManager.NameNotFoundException ex) {
+      // Should never happen
+      Log.e(TAG, "Can't find our own package", ex);
+    }
+
+    return sb.toString();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  AudioOutputManager getAudioOutputManager() {
+    return audioOutputManager;
+  }
+
+  /** Returns Java layer implementation for AndroidVoiceRecognizer */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  VoiceRecognizer getVoiceRecognizer() {
+    return voiceRecognizer;
+  }
+
+  /** Returns Java layer implementation for AudioPermissionRequester */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  AudioPermissionRequester getAudioPermissionRequester() {
+    return audioPermissionRequester;
+  }
+
+  void onActivityResult(int requestCode, int resultCode, Intent data) {
+    userAuthorizer.onActivityResult(requestCode, resultCode, data);
+  }
+
+  void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+    userAuthorizer.onRequestPermissionsResult(requestCode, permissions, grantResults);
+    audioPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void setVideoSurfaceBounds(final int x, final int y, final int width, final int height) {
+    Activity activity = activityHolder.get();
+    if (activity instanceof CobaltActivity) {
+      ((CobaltActivity) activity).setVideoSurfaceBounds(x, y, width, height);
+    }
+  }
+
+  /**
+   * Check if hdrType is supported by the current default display. See
+   * https://developer.android.com/reference/android/view/Display.HdrCapabilities.html for valid
+   * values.
+   */
+  @TargetApi(24)
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public boolean isHdrTypeSupported(int hdrType) {
+    if (android.os.Build.VERSION.SDK_INT < 24) {
+      return false;
+    }
+
+    Activity activity = activityHolder.get();
+    if (activity == null) {
+      return false;
+    }
+
+    WindowManager windowManager = activity.getWindowManager();
+    if (windowManager == null) {
+      return false;
+    }
+
+    int[] supportedHdrTypes =
+        windowManager.getDefaultDisplay().getHdrCapabilities().getSupportedHdrTypes();
+    for (int supportedType : supportedHdrTypes) {
+      if (supportedType == hdrType) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
new file mode 100644
index 0000000..d9aba31
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
@@ -0,0 +1,193 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.coat;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.speech.RecognitionListener;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+
+/**
+ * This class uses Android's SpeechRecognizer to perform speech recognition. Using Android's
+ * platform recognizer offers several benefits such as good quality and good local fallback when no
+ * data connection is available.
+ */
+public class VoiceRecognizer {
+  private final Context context;
+  private final Holder<Activity> activityHolder;
+  private final Handler mainHandler = new Handler(Looper.getMainLooper());
+  private final AudioPermissionRequester audioPermissionRequester;
+  private SpeechRecognizer speechRecognizer;
+
+  // Native pointer to C++ SbSpeechRecognizerImpl.
+  private long nativeSpeechRecognizerImpl;
+
+  // Remember if we are using continuous recognition.
+  private boolean continuous;
+  private boolean interimResults;
+  private int maxAlternatives;
+
+  // Internal class to handle events from Android's SpeechRecognizer and route
+  // them to native.
+  class Listener implements RecognitionListener {
+    @Override
+    public void onBeginningOfSpeech() {
+      nativeOnSpeechDetected(nativeSpeechRecognizerImpl, true);
+    }
+
+    @Override
+    public void onBufferReceived(byte[] buffer) {}
+
+    @Override
+    public void onEndOfSpeech() {
+      nativeOnSpeechDetected(nativeSpeechRecognizerImpl, false);
+    }
+
+    @Override
+    public void onError(int error) {
+      nativeOnError(nativeSpeechRecognizerImpl, error);
+      reset();
+    }
+
+    @Override
+    public void onEvent(int eventType, Bundle params) {}
+
+    @Override
+    public void onPartialResults(Bundle bundle) {
+      handleResults(bundle, false);
+    }
+
+    @Override
+    public void onReadyForSpeech(Bundle params) {}
+
+    @Override
+    public void onResults(Bundle bundle) {
+      handleResults(bundle, true);
+      reset();
+    }
+
+    @Override
+    public void onRmsChanged(float rmsdB) {}
+
+    private void handleResults(Bundle bundle, boolean isFinal) {
+      ArrayList<String> list = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+      String[] results = list.toArray(new String[list.size()]);
+      float[] scores = bundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES);
+      nativeOnResults(nativeSpeechRecognizerImpl, results, scores, isFinal);
+    }
+  };
+
+  public VoiceRecognizer(
+      Context context,
+      Holder<Activity> activityHolder,
+      AudioPermissionRequester audioPermissionRequester) {
+    this.context = context;
+    this.activityHolder = activityHolder;
+    this.audioPermissionRequester = audioPermissionRequester;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void startRecognition(
+      boolean continuous,
+      boolean interimResults,
+      int maxAlternatives,
+      long nativeSpeechRecognizer) {
+    this.continuous = continuous;
+    this.interimResults = interimResults;
+    this.maxAlternatives = maxAlternatives;
+    this.nativeSpeechRecognizerImpl = nativeSpeechRecognizer;
+
+    if (this.audioPermissionRequester.requestRecordAudioPermission(
+        this.nativeSpeechRecognizerImpl)) {
+      startRecognitionInternal();
+    } else {
+      mainHandler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              nativeOnError(
+                  nativeSpeechRecognizerImpl, SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS);
+            }
+          });
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void stopRecognition() {
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            if (Looper.myLooper() != Looper.getMainLooper()) {
+              throw new RuntimeException("Must be called in main thread.");
+            }
+            if (speechRecognizer == null) {
+              return;
+            }
+            reset();
+          }
+        };
+    mainHandler.post(runnable);
+  }
+
+  private void startRecognitionInternal() {
+    Runnable runnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            if (Looper.myLooper() != Looper.getMainLooper()) {
+              throw new RuntimeException("Must be called in main thread.");
+            }
+            speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
+            speechRecognizer.setRecognitionListener(new Listener());
+            Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+            intent.putExtra(
+                RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+            intent.putExtra("android.speech.extra.DICTATION_MODE", continuous);
+            intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, interimResults);
+            intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxAlternatives);
+            speechRecognizer.startListening(intent);
+          }
+        };
+    mainHandler.post(runnable);
+  }
+
+  private void reset() {
+    speechRecognizer.destroy();
+    speechRecognizer = null;
+
+    nativeSpeechRecognizerImpl = 0;
+    continuous = false;
+    interimResults = false;
+    maxAlternatives = 1;
+  }
+
+  private native void nativeOnSpeechDetected(long nativeSpeechRecognizerImpl, boolean detected);
+
+  private native void nativeOnError(long nativeSpeechRecognizerImpl, int error);
+
+  private native void nativeOnResults(
+      long nativeSpeechRecognizerImpl, String[] results, float[] confidences, boolean isFinal);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java
new file mode 100644
index 0000000..66a2547
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/FeedbackService.java
@@ -0,0 +1,36 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.feedback;
+
+import android.graphics.Bitmap;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+
+/** Java side implementation for starboard::android::shared::cobalt::FeedbackService. */
+public interface FeedbackService {
+  /**
+   * Sends the given product specific data to the GMS Feedback Service.
+   *
+   * <p>Implementations must annotate this method with @UsedByNative so Proguard doesn't remove it.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void sendFeedback(
+      HashMap<String, String> productSpecificData, String categoryTag, Bitmap screenshot);
+
+  void connect();
+
+  void disconnect();
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java
new file mode 100644
index 0000000..0c59c84
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/feedback/NoopFeedbackService.java
@@ -0,0 +1,49 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.feedback;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+
+/** FeedbackService implementation that doesn't send any feedback. */
+public class NoopFeedbackService implements FeedbackService {
+  @Override
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void sendFeedback(
+      HashMap<String, String> productSpecificData, String categoryTag, Bitmap screenshot) {
+    Log.i(TAG, "Feedback product specific data:");
+    for (String key : productSpecificData.keySet()) {
+      Log.i(TAG, key + ": " + productSpecificData.get(key));
+    }
+    if (screenshot != null) {
+      Log.i(TAG, "Screenshot dimensions: " + screenshot.getWidth() + "x" + screenshot.getHeight());
+    }
+    if (!TextUtils.isEmpty(categoryTag)) {
+      Log.i(TAG, "Category tag: " + categoryTag);
+    }
+  }
+
+  @Override
+  public void connect() {}
+
+  @Override
+  public void disconnect() {}
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
new file mode 100644
index 0000000..d76266f
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/ArtworkLoader.java
@@ -0,0 +1,167 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.util.Pair;
+import android.util.Size;
+import dev.cobalt.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/** Loads MediaImage artwork, and caches one image. */
+public class ArtworkLoader {
+
+  /** Callback to receive the image loaded in the background by getOrLoadArtwork() */
+  public interface Callback {
+    void onArtworkLoaded(Bitmap bitmap);
+  }
+
+  @NonNull private volatile String requestedArtworkUrl = "";
+  @NonNull private volatile String currentArtworkUrl = "";
+  private volatile Bitmap currentArtwork = null;
+
+  private final Callback callback;
+  private final Size displaySize;
+
+  public ArtworkLoader(Callback callback, Size displaySize) {
+    this.callback = callback;
+    this.displaySize = displaySize;
+  }
+
+  /**
+   * Returns a cached image if available. If not cached, returns null and starts downloading it in
+   * the background, and then when ready the callback will be called with the image.
+   */
+  public synchronized Bitmap getOrLoadArtwork(MediaImage[] artwork) {
+    MediaImage image = getBestFitImage(artwork);
+    String url = (image == null) ? "" : image.src;
+
+    // Check if this artwork is already loaded or requested.
+    if (url.equals(currentArtworkUrl)) {
+      return currentArtwork;
+    } else if (url.equals(requestedArtworkUrl)) {
+      return null;
+    }
+
+    requestedArtworkUrl = url;
+    new DownloadArtworkTask().execute(url);
+    return null;
+  }
+
+  /**
+   * Returns the image that most closely matches the display size, or null if there are no images.
+   * We don't really know what size view the image may appear in (on the Now Playing card on Android
+   * TV launcher, or any other observer of the MediaSession), so we use display size as the largest
+   * useful size on any particular device.
+   */
+  private MediaImage getBestFitImage(MediaImage[] artwork) {
+    if (artwork == null || artwork.length == 0) {
+      return null;
+    }
+    MediaImage bestImage = artwork[0];
+    int minDiagonalSquared = Integer.MAX_VALUE;
+    for (MediaImage image : artwork) {
+      Size imageSize = parseImageSize(image);
+      int widthDelta = displaySize.getWidth() - imageSize.getWidth();
+      int heightDelta = displaySize.getHeight() - imageSize.getHeight();
+      int diagonalSquared = widthDelta * widthDelta + heightDelta * heightDelta;
+      if (diagonalSquared < minDiagonalSquared) {
+        bestImage = image;
+        minDiagonalSquared = diagonalSquared;
+      }
+    }
+    return bestImage;
+  }
+
+  private Size parseImageSize(MediaImage image) {
+    try {
+      String sizeStr = image.sizes.split("\\s+", -1)[0];
+      return Size.parseSize(sizeStr.toLowerCase());
+    } catch (NumberFormatException | NullPointerException e) {
+      return new Size(0, 0);
+    }
+  }
+
+  private synchronized void onDownloadFinished(Pair<String, Bitmap> urlBitmapPair) {
+    String url = urlBitmapPair.first;
+    Bitmap bitmap = urlBitmapPair.second;
+    if (url.equals(requestedArtworkUrl)) {
+      requestedArtworkUrl = "";
+      if (bitmap != null) {
+        currentArtworkUrl = url;
+        currentArtwork = bitmap;
+        callback.onArtworkLoaded(bitmap);
+      }
+    }
+  }
+
+  private class DownloadArtworkTask extends AsyncTask<String, Void, Pair<String, Bitmap>> {
+
+    @Override
+    protected Pair<String, Bitmap> doInBackground(String... params) {
+      String url = params[0];
+      Bitmap bitmap = null;
+      HttpURLConnection conn = null;
+      InputStream is = null;
+      try {
+        conn = (HttpURLConnection) new URL(url).openConnection();
+        is = conn.getInputStream();
+        bitmap = BitmapFactory.decodeStream(is);
+      } catch (IOException e) {
+        Log.e(TAG, "Could not download artwork", e);
+      } finally {
+        try {
+          if (conn != null) {
+            conn.disconnect();
+          }
+          if (is != null) {
+            is.close();
+          }
+        } catch (Exception e) {
+          Log.e(TAG, "Error closing connection for artwork", e);
+        }
+      }
+
+      // Crop to 16:9 as needed
+      if (bitmap != null) {
+        int height = bitmap.getWidth() * 9 / 16;
+        if (bitmap.getHeight() > height) {
+          int top = (bitmap.getHeight() - height) / 2;
+          bitmap = Bitmap.createBitmap(bitmap, 0, top, bitmap.getWidth(), height);
+        }
+      }
+
+      return Pair.create(url, bitmap);
+    }
+
+    @Override
+    protected void onPostExecute(Pair<String, Bitmap> urlBitmapPair) {
+      onDownloadFinished(urlBitmapPair);
+    }
+
+    @Override
+    protected void onCancelled(Pair<String, Bitmap> urlBitmapPair) {
+      onDownloadFinished(urlBitmapPair);
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
new file mode 100644
index 0000000..0a72bc8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
@@ -0,0 +1,108 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Build;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Creates and destroys AudioTrackBridge and handles the volume change. */
+public class AudioOutputManager implements CobaltMediaSession.UpdateVolumeListener {
+  private List<AudioTrackBridge> audioTrackBridgeList;
+  private Context context;
+
+  public AudioOutputManager(Context context) {
+    this.context = context;
+    audioTrackBridgeList = new ArrayList<AudioTrackBridge>();
+  }
+
+  @Override
+  public void onUpdateVolume(float gain) {
+    for (AudioTrackBridge audioTrackBridge : audioTrackBridgeList) {
+      audioTrackBridge.setVolume(gain);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  AudioTrackBridge createAudioTrackBridge(
+      int sampleType, int sampleRate, int channelCount, int framesPerChannel) {
+    AudioTrackBridge audioTrackBridge =
+        new AudioTrackBridge(sampleType, sampleRate, channelCount, framesPerChannel);
+    if (!audioTrackBridge.isAudioTrackValid()) {
+      Log.e(TAG, "AudioTrackBridge has invalid audio track");
+      return null;
+    }
+    audioTrackBridgeList.add(audioTrackBridge);
+    return audioTrackBridge;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  void destroyAudioTrackBridge(AudioTrackBridge audioTrackBridge) {
+    audioTrackBridge.release();
+    audioTrackBridgeList.remove(audioTrackBridge);
+  }
+
+  /** Returns the maximum number of HDMI channels. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  int getMaxChannels() {
+    // The aac audio decoder on this platform will switch its output from 5.1
+    // to stereo right before providing the first output buffer when
+    // attempting to decode 5.1 input.  Since this heavily violates invariants
+    // of the shared starboard player framework, disable 5.1 on this platform.
+    // It is expected that we will be able to resolve this issue with Xiaomi
+    // by Android P, so only do this workaround for SDK versions < 27.
+    if (android.os.Build.MODEL.equals("MIBOX3") && android.os.Build.VERSION.SDK_INT < 27) {
+      return 2;
+    }
+
+    if (Build.VERSION.SDK_INT >= 23) {
+      return getMaxChannelsV23();
+    }
+    return 2;
+  }
+
+  /** Returns the maximum number of HDMI channels for API 23 and above. */
+  @TargetApi(23)
+  private int getMaxChannelsV23() {
+    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+    int maxChannels = 2;
+    for (AudioDeviceInfo info : deviceInfos) {
+      int type = info.getType();
+      if (type == AudioDeviceInfo.TYPE_HDMI || type == AudioDeviceInfo.TYPE_HDMI_ARC) {
+        int[] channelCounts = info.getChannelCounts();
+        if (channelCounts.length == 0) {
+          // An empty array indicates that the device supports arbitrary channel masks.
+          return 8;
+        }
+        for (int count : channelCounts) {
+          maxChannels = Math.max(maxChannels, count);
+        }
+      }
+    }
+    return maxChannels;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
new file mode 100644
index 0000000..54d00ac
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -0,0 +1,204 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTimestamp;
+import android.media.AudioTrack;
+import android.os.Build;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+
+/** A wrapper of the android AudioTrack class. */
+@UsedByNative
+public class AudioTrackBridge {
+  private AudioTrack audioTrack;
+  private AudioTimestamp audioTimestamp = new AudioTimestamp();
+  private long maxFramePositionSoFar = 0;
+
+  public AudioTrackBridge(int sampleType, int sampleRate, int channelCount, int framesPerChannel) {
+    int channelConfig;
+    switch (channelCount) {
+      case 1:
+        channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+        break;
+      case 2:
+        channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+        break;
+      case 6:
+        channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+        break;
+      default:
+        throw new RuntimeException("Unsupported channel count: " + channelCount);
+    }
+
+    AudioAttributes attributes =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    AudioFormat format =
+        new AudioFormat.Builder()
+            .setEncoding(sampleType)
+            .setSampleRate(sampleRate)
+            .setChannelMask(channelConfig)
+            .build();
+
+    int minBufferSizeBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType);
+    int audioTrackBufferSize = minBufferSizeBytes;
+    // Use framesPerChannel to determine the buffer size.  To use a large buffer on a small
+    // framesPerChannel may lead to audio playback not able to start.
+    while (audioTrackBufferSize < framesPerChannel) {
+      audioTrackBufferSize *= 2;
+    }
+    while (audioTrackBufferSize > 0) {
+      try {
+        audioTrack =
+            new AudioTrack(
+                attributes,
+                format,
+                audioTrackBufferSize,
+                AudioTrack.MODE_STREAM,
+                AudioManager.AUDIO_SESSION_ID_GENERATE);
+      } catch (Exception e) {
+        audioTrack = null;
+      }
+      // AudioTrack ctor can fail in multiple, platform specific ways, so do a thorough check
+      // before proceed.
+      if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
+        break;
+      }
+      audioTrackBufferSize /= 2;
+    }
+    Log.i(
+        TAG,
+        String.format(
+            "AudioTrack created with buffer size %d.  The minimum buffer size is %d.",
+            audioTrackBufferSize, minBufferSizeBytes));
+  }
+
+  public Boolean isAudioTrackValid() {
+    return audioTrack != null;
+  }
+
+  public void release() {
+    if (audioTrack != null) {
+      audioTrack.release();
+    }
+    audioTrack = null;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public int setVolume(float gain) {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to setVolume with NULL audio track.");
+      return 0;
+    }
+    return audioTrack.setVolume(gain);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void play() {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to play with NULL audio track.");
+      return;
+    }
+    audioTrack.play();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void pause() {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to pause with NULL audio track.");
+      return;
+    }
+    audioTrack.pause();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void flush() {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to flush with NULL audio track.");
+      return;
+    }
+    audioTrack.flush();
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private int write(byte[] audioData, int sizeInBytes) {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to write with NULL audio track.");
+      return 0;
+    }
+    if (Build.VERSION.SDK_INT >= 23) {
+      return audioTrack.write(audioData, 0, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING);
+    } else {
+      ByteBuffer byteBuffer = ByteBuffer.wrap(audioData);
+      return audioTrack.write(byteBuffer, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private int write(float[] audioData, int sizeInFloats) {
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to write with NULL audio track.");
+      return 0;
+    }
+    return audioTrack.write(audioData, 0, sizeInFloats, AudioTrack.WRITE_NON_BLOCKING);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private AudioTimestamp getAudioTimestamp() {
+    // TODO: Consider calling with TIMEBASE_MONOTONIC and returning that
+    // information to the starboard audio sink.
+    if (audioTrack == null) {
+      Log.e(TAG, "Unable to getAudioTimestamp with NULL audio track.");
+      return audioTimestamp;
+    }
+    if (audioTrack.getTimestamp(audioTimestamp)) {
+      // This conversion is safe, as only the lower bits will be set, since we
+      // called |getTimestamp| without a timebase.
+      // https://developer.android.com/reference/android/media/AudioTimestamp.html#framePosition
+      audioTimestamp.framePosition = (int) audioTimestamp.framePosition;
+    } else {
+      // Time stamps haven't been updated yet, assume playback hasn't started.
+      audioTimestamp.framePosition = 0;
+      audioTimestamp.nanoTime = System.nanoTime();
+    }
+
+    // TODO: This is required for correctness of the audio sink, because
+    // otherwise we would be going back in time. Investigate the impact it has
+    // on playback.  All empirical measurements so far suggest that it should
+    // be negligible.
+    if (audioTimestamp.framePosition < maxFramePositionSoFar) {
+      audioTimestamp.framePosition = maxFramePositionSoFar;
+    }
+    maxFramePositionSoFar = audioTimestamp.framePosition;
+
+    return audioTimestamp;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java
new file mode 100644
index 0000000..43d6997
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CaptionSettings.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import android.view.accessibility.CaptioningManager;
+import dev.cobalt.util.UsedByNative;
+
+/**
+ * Captures the system Caption style in properties as needed by the Starboard implementation.
+ */
+public class CaptionSettings {
+
+  @UsedByNative public final boolean isEnabled;
+  @UsedByNative public final float fontScale;
+  @UsedByNative public final int edgeType;
+  @UsedByNative public final boolean hasEdgeType;
+  @UsedByNative public final int foregroundColor;
+  @UsedByNative public final boolean hasForegroundColor;
+  @UsedByNative public final int backgroundColor;
+  @UsedByNative public final boolean hasBackgroundColor;
+  @UsedByNative public final int windowColor;
+  @UsedByNative public final boolean hasWindowColor;
+
+  public CaptionSettings(CaptioningManager cm) {
+    CaptioningManager.CaptionStyle style = cm.getUserStyle();
+    isEnabled = cm.isEnabled();
+    fontScale = cm.getFontScale();
+    edgeType = style.edgeType;
+    hasEdgeType = style.hasEdgeType();
+    foregroundColor = style.foregroundColor;
+    hasForegroundColor = style.hasForegroundColor();
+    backgroundColor = style.backgroundColor;
+    hasBackgroundColor = style.hasBackgroundColor();
+    windowColor = style.windowColor;
+    hasWindowColor = style.hasWindowColor();
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
new file mode 100644
index 0000000..8ca2a31
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
@@ -0,0 +1,440 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.AudioAttributes;
+import android.media.AudioAttributes.Builder;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.WindowManager;
+import dev.cobalt.util.DisplayUtil;
+import dev.cobalt.util.Holder;
+import dev.cobalt.util.Log;
+
+/**
+ * Cobalt MediaSession glue, as well as collection of state and logic to switch on/off Android OS
+ * features used in media playback, such as audio focus, "KEEP_SCREEN_ON" mode, and "visible
+ * behind".
+ */
+public class CobaltMediaSession
+    implements AudioManager.OnAudioFocusChangeListener, ArtworkLoader.Callback {
+
+  // We do handle transport controls and set this flag on all API levels, even though it's
+  // deprecated and unnecessary on API 26+.
+  @SuppressWarnings("deprecation")
+  private static final int MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS =
+      MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
+
+  private AudioFocusRequest audioFocusRequest;
+
+  interface UpdateVolumeListener {
+    /** Called when there is a change in audio focus. */
+    void onUpdateVolume(float gain);
+  }
+
+  /**
+   * When losing audio focus with the option of ducking, we reduce the volume to 10%. This arbitrary
+   * number is what YouTube Android Player infrastructure uses.
+   */
+  private static final float AUDIO_FOCUS_DUCK_LEVEL = 0.1f;
+
+  private final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+  private final Context context;
+  private final Holder<Activity> activityHolder;
+
+  private final UpdateVolumeListener volumeListener;
+  private final ArtworkLoader artworkLoader;
+  private MediaSession mediaSession;
+
+  // We re-use the builder to hold onto the most recent metadata and add artwork later.
+  private MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
+  // We re-use the builder to hold onto the most recent playback state.
+  private PlaybackState.Builder playbackStateBuilder = new PlaybackState.Builder();
+
+  // Duplicated in starboard/android/shared/android_media_session_client.h
+  // PlaybackState
+  private static final int PLAYBACK_STATE_PLAYING = 0;
+  private static final int PLAYBACK_STATE_PAUSED = 1;
+  private static final int PLAYBACK_STATE_NONE = 2;
+  private static final String[] PLAYBACK_STATE_NAME = {"playing", "paused", "none"};
+
+  // Accessed on the main looper thread only.
+  private int playbackState = PLAYBACK_STATE_NONE;
+  private boolean transientPause = false;
+  private boolean suspended = true;
+
+  public CobaltMediaSession(
+      Context context, Holder<Activity> activityHolder, UpdateVolumeListener volumeListener) {
+    this.context = context;
+    this.activityHolder = activityHolder;
+
+    this.volumeListener = volumeListener;
+    artworkLoader = new ArtworkLoader(this, DisplayUtil.getDisplaySize(context));
+    setMediaSession();
+  }
+
+  private void setMediaSession() {
+    mediaSession = new MediaSession(context, TAG);
+    mediaSession.setFlags(MEDIA_SESSION_FLAG_HANDLES_TRANSPORT_CONTROLS);
+    mediaSession.setCallback(
+        new MediaSession.Callback() {
+          @Override
+          public void onFastForward() {
+            Log.i(TAG, "MediaSession action: FAST FORWARD");
+            nativeInvokeAction(PlaybackState.ACTION_FAST_FORWARD);
+          }
+
+          @Override
+          public void onPause() {
+            Log.i(TAG, "MediaSession action: PAUSE");
+            nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+          }
+
+          @Override
+          public void onPlay() {
+            Log.i(TAG, "MediaSession action: PLAY");
+            nativeInvokeAction(PlaybackState.ACTION_PLAY);
+          }
+
+          @Override
+          public void onRewind() {
+            Log.i(TAG, "MediaSession action: REWIND");
+            nativeInvokeAction(PlaybackState.ACTION_REWIND);
+          }
+
+          @Override
+          public void onSkipToNext() {
+            Log.i(TAG, "MediaSession action: SKIP NEXT");
+            nativeInvokeAction(PlaybackState.ACTION_SKIP_TO_NEXT);
+          }
+
+          @Override
+          public void onSkipToPrevious() {
+            Log.i(TAG, "MediaSession action: SKIP PREVIOUS");
+            nativeInvokeAction(PlaybackState.ACTION_SKIP_TO_PREVIOUS);
+          }
+
+          @Override
+          public void onSeekTo(long pos) {
+            Log.i(TAG, "MediaSession action: SEEK " + pos);
+            nativeInvokeAction(PlaybackState.ACTION_SEEK_TO, pos);
+          }
+        });
+    // |metadataBuilder| may still have no fields at this point, yielding empty metadata.
+    mediaSession.setMetadata(metadataBuilder.build());
+    // |playbackStateBuilder| may still have no fields at this point.
+    mediaSession.setPlaybackState(playbackStateBuilder.build());
+  }
+
+  private static void checkMainLooperThread() {
+    if (Looper.getMainLooper() != Looper.myLooper()) {
+      throw new RuntimeException("Must be on main thread");
+    }
+  }
+
+  /**
+   * Sets system media resources active or not according to whether media is playing. This is
+   * idempotent as it may be called multiple times during the course of a media session.
+   */
+  private void configureMediaFocus(int playbackState) {
+    checkMainLooperThread();
+    if (transientPause && playbackState == PLAYBACK_STATE_PAUSED) {
+      Log.i(TAG, "Media focus: paused (transient)");
+      // Don't release media focus while transiently paused, otherwise we won't get audiofocus back
+      // when the transient condition ends and we would leave playback paused.
+      return;
+    }
+    Log.i(TAG, "Media focus: " + PLAYBACK_STATE_NAME[playbackState]);
+    wakeLock(playbackState == PLAYBACK_STATE_PLAYING);
+    audioFocus(playbackState == PLAYBACK_STATE_PLAYING);
+
+    boolean activating = playbackState != PLAYBACK_STATE_NONE && !mediaSession.isActive();
+    boolean deactivating = playbackState == PLAYBACK_STATE_NONE && mediaSession.isActive();
+    if (activating) {
+      // Resuming or new playbacks land here.
+      setMediaSession();
+    }
+    mediaSession.setActive(playbackState != PLAYBACK_STATE_NONE);
+    if (deactivating) {
+      // Suspending lands here.
+      mediaSession.release();
+    }
+  }
+
+  private void wakeLock(boolean lock) {
+    Activity activity = activityHolder.get();
+    if (activity == null) {
+      return;
+    }
+    if (lock) {
+      activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    } else {
+      activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+  }
+
+  private void audioFocus(boolean focus) {
+    if (focus) {
+      int res;
+      if (Build.VERSION.SDK_INT < 26) {
+        res = requestAudioFocus();
+      } else {
+        res = requestAudioFocusV26();
+      }
+      // This shouldn't happen, but pause playback to be nice if it does.
+      if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+        Log.w(TAG, "Audiofocus action: PAUSE (not granted)");
+        nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+      }
+    } else {
+      if (Build.VERSION.SDK_INT < 26) {
+        abandonAudioFocus();
+      } else {
+        abandonAudioFocusV26();
+      }
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  private int requestAudioFocus() {
+    return getAudioManager()
+        .requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+  }
+
+  @TargetApi(26)
+  private int requestAudioFocusV26() {
+    if (audioFocusRequest == null) {
+      AudioAttributes audioAtrributes =
+          new Builder().setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+      audioFocusRequest =
+          new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+              .setOnAudioFocusChangeListener(this)
+              .setAudioAttributes(audioAtrributes)
+              .build();
+    }
+    return getAudioManager().requestAudioFocus(audioFocusRequest);
+  }
+
+  @SuppressWarnings("deprecation")
+  private void abandonAudioFocus() {
+    getAudioManager().abandonAudioFocus(this);
+  }
+
+  @TargetApi(26)
+  private void abandonAudioFocusV26() {
+    if (audioFocusRequest != null) {
+      getAudioManager().abandonAudioFocusRequest(audioFocusRequest);
+    }
+  }
+
+  /** AudioManager.OnAudioFocusChangeListener implementation. */
+  @Override
+  public void onAudioFocusChange(int focusChange) {
+    String logExtra = "";
+    switch (focusChange) {
+      case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+        logExtra = " (transient)";
+        // fall through
+      case AudioManager.AUDIOFOCUS_LOSS:
+        Log.i(TAG, "Audiofocus loss" + logExtra);
+        if (playbackState == PLAYBACK_STATE_PLAYING) {
+          Log.i(TAG, "Audiofocus action: PAUSE");
+          nativeInvokeAction(PlaybackState.ACTION_PAUSE);
+        }
+        break;
+      case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+        Log.i(TAG, "Audiofocus duck");
+        // Lower the volume, keep current play state.
+        // Starting with API 26 the system does automatic ducking without calling our listener,
+        // but we still need this for API < 26.
+        volumeListener.onUpdateVolume(AUDIO_FOCUS_DUCK_LEVEL);
+        break;
+      case AudioManager.AUDIOFOCUS_GAIN:
+        Log.i(TAG, "Audiofocus gain");
+        // The app has been granted audio focus (again). Raise volume to normal,
+        // restart playback if necessary.
+        volumeListener.onUpdateVolume(1.0f);
+        if (transientPause && playbackState == PLAYBACK_STATE_PAUSED) {
+          Log.i(TAG, "Audiofocus action: PLAY");
+          nativeInvokeAction(PlaybackState.ACTION_PLAY);
+        }
+        break;
+      default: // fall out
+    }
+
+    // Keep track of whether we're currently paused because of a transient loss of audiofocus.
+    transientPause = (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+  }
+
+  private AudioManager getAudioManager() {
+    return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+  }
+
+  public void resume() {
+    mainHandler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            resumeInternal();
+          }
+        });
+  }
+
+  private void resumeInternal() {
+    checkMainLooperThread();
+    suspended = false;
+    // Undoing what may have been done in suspendInternal().
+    configureMediaFocus(playbackState);
+  }
+
+  public void suspend() {
+    mainHandler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            suspendInternal();
+          }
+        });
+  }
+
+  private void suspendInternal() {
+    checkMainLooperThread();
+    suspended = true;
+
+    // We generally believe the HTML5 app playback state as the source of truth for configuring
+    // media focus since only it can know about a momentary pause between videos in a playlist, or
+    // other autoplay scenario when we should keep media focus. However, when suspending, any
+    // active SbPlayer is destroyed and we release media focus, even if the HTML5 app still thinks
+    // it's in a playing state. We'll configure it again in resumeInternal() and the HTML5 app will
+    // be none the wiser.
+    playbackStateBuilder.setState(
+        playbackState,
+        PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+        playbackState == PLAYBACK_STATE_PLAYING ? 1.0f : 0.0f);
+    configureMediaFocus(PLAYBACK_STATE_NONE);
+  }
+
+  private static void nativeInvokeAction(long action) {
+    nativeInvokeAction(action, 0);
+  }
+
+  private static native void nativeInvokeAction(long action, long seekMs);
+
+  public void updateMediaSession(
+      final int playbackState,
+      final long actions,
+      final long positionMs,
+      final float speed,
+      final String title,
+      final String artist,
+      final String album,
+      final MediaImage[] artwork) {
+    mainHandler.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            updateMediaSessionInternal(
+                playbackState, actions, positionMs, speed,
+                title, artist, album, artwork);
+          }
+        });
+  }
+
+  /** Called on main looper thread when media session changes. */
+  private void updateMediaSessionInternal(
+      int playbackState,
+      long actions,
+      long positionMs,
+      float speed,
+      String title,
+      String artist,
+      String album,
+      MediaImage[] artwork) {
+    checkMainLooperThread();
+
+    // Always keep track of what the HTML5 app thinks the playback state is so we can configure the
+    // media focus correctly, either immediately or when resuming from being suspended.
+    this.playbackState = playbackState;
+
+    // Don't update anything while suspended.
+    if (suspended) {
+      Log.i(TAG, "Playback state change while suspended: " + PLAYBACK_STATE_NAME[playbackState]);
+      return;
+    }
+
+    configureMediaFocus(playbackState);
+
+    // Ignore updates to the MediaSession metadata if playback is stopped.
+    if (playbackState == PLAYBACK_STATE_NONE) {
+      return;
+    }
+
+    int androidPlaybackState;
+    String stateName;
+    switch (playbackState) {
+      case PLAYBACK_STATE_PLAYING:
+        androidPlaybackState = PlaybackState.STATE_PLAYING;
+        stateName = "PLAYING";
+        break;
+      case PLAYBACK_STATE_PAUSED:
+        androidPlaybackState = PlaybackState.STATE_PAUSED;
+        stateName = "PAUSED";
+        break;
+      case PLAYBACK_STATE_NONE:
+      default:
+        androidPlaybackState = PlaybackState.STATE_NONE;
+        stateName = "NONE";
+        break;
+    }
+
+    Log.i(TAG, String.format(
+        "MediaSession state: %s, position: %d ms, speed: %f x", stateName, positionMs, speed));
+
+    playbackStateBuilder =
+        new PlaybackState.Builder()
+            .setActions(actions)
+            .setState(androidPlaybackState, positionMs, speed);
+    mediaSession.setPlaybackState(playbackStateBuilder.build());
+
+    // Reset the metadata to make sure we don't retain any fields from previous playback.
+    metadataBuilder = new MediaMetadata.Builder();
+    metadataBuilder
+        .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+        .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
+        .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
+        .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, artworkLoader.getOrLoadArtwork(artwork));
+    // Update the metadata as soon as we can - even before artwork is loaded.
+    mediaSession.setMetadata(metadataBuilder.build());
+  }
+
+  @Override
+  public void onArtworkLoaded(Bitmap bitmap) {
+    metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
+    mediaSession.setMetadata(metadataBuilder.build());
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java
new file mode 100644
index 0000000..44e51b8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+/**
+ * Common definitions for logging in the media package.
+ */
+public class Log {
+  public static final String TAG = "starboard_media";
+
+  private Log() {}
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
new file mode 100644
index 0000000..5fd0a8b
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
@@ -0,0 +1,953 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.AudioFormat;
+import android.media.MediaCodec;
+import android.media.MediaCodec.CryptoInfo;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Surface;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+
+/** A wrapper of the MediaCodec class. */
+@SuppressWarnings("unused")
+@UsedByNative
+class MediaCodecBridge {
+  // Error code for MediaCodecBridge. Keep this value in sync with
+  // MEDIA_CODEC_* values in media_codec_bridge.h.
+  private static final int MEDIA_CODEC_OK = 0;
+  private static final int MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER = 1;
+  private static final int MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER = 2;
+  private static final int MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED = 3;
+  private static final int MEDIA_CODEC_OUTPUT_FORMAT_CHANGED = 4;
+  private static final int MEDIA_CODEC_INPUT_END_OF_STREAM = 5;
+  private static final int MEDIA_CODEC_OUTPUT_END_OF_STREAM = 6;
+  private static final int MEDIA_CODEC_NO_KEY = 7;
+  private static final int MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION = 8;
+  private static final int MEDIA_CODEC_ABORT = 9;
+  private static final int MEDIA_CODEC_ERROR = 10;
+
+  // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps
+  // for several frames. As a result, the player may find that the time does not increase
+  // after decoding a frame. To detect this, we check whether the presentation timestamp from
+  // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US
+  // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be
+  // non-decreasing for the remaining frames.
+  private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000;
+
+  // We use only one output audio format (PCM16) that has 2 bytes per sample
+  private static final int PCM16_BYTES_PER_SAMPLE = 2;
+
+  // TODO: Use MediaFormat constants when part of the public API.
+  private static final String KEY_CROP_LEFT = "crop-left";
+  private static final String KEY_CROP_RIGHT = "crop-right";
+  private static final String KEY_CROP_BOTTOM = "crop-bottom";
+  private static final String KEY_CROP_TOP = "crop-top";
+
+  private static final int BITRATE_ADJUSTMENT_FPS = 30;
+  private static final int MAXIMUM_INITIAL_FPS = 30;
+
+  private MediaCodec mMediaCodec;
+  private boolean mFlushed;
+  private long mLastPresentationTimeUs;
+  private final String mMime;
+  private boolean mAdaptivePlaybackSupported;
+
+  // Functions that require this will be called frequently in a tight loop.
+  // Only create one of these and reuse it to avoid excessive allocations,
+  // which would cause GC cycles long enough to impact playback.
+  private final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+
+  // Type of bitrate adjustment for video encoder.
+  public enum BitrateAdjustmentTypes {
+    // No adjustment - video encoder has no known bitrate problem.
+    NO_ADJUSTMENT,
+    // Framerate based bitrate adjustment is required - HW encoder does not use frame
+    // timestamps to calculate frame bitrate budget and instead is relying on initial
+    // fps configuration assuming that all frames are coming at fixed initial frame rate.
+    FRAMERATE_ADJUSTMENT,
+  }
+
+  public static final class MimeTypes {
+    public static final String VIDEO_MP4 = "video/mp4";
+    public static final String VIDEO_WEBM = "video/webm";
+    public static final String VIDEO_H264 = "video/avc";
+    public static final String VIDEO_H265 = "video/hevc";
+    public static final String VIDEO_VP8 = "video/x-vnd.on2.vp8";
+    public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
+  }
+
+  private BitrateAdjustmentTypes mBitrateAdjustmentType = BitrateAdjustmentTypes.NO_ADJUSTMENT;
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static class DequeueInputResult {
+    private int mStatus;
+    private int mIndex;
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private DequeueInputResult() {
+      mStatus = MEDIA_CODEC_ERROR;
+      mIndex = -1;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private DequeueInputResult(int status, int index) {
+      mStatus = status;
+      mIndex = index;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int status() {
+      return mStatus;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int index() {
+      return mIndex;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private static class DequeueOutputResult {
+    private int mStatus;
+    private int mIndex;
+    private int mFlags;
+    private int mOffset;
+    private long mPresentationTimeMicroseconds;
+    private int mNumBytes;
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private DequeueOutputResult() {
+      mStatus = MEDIA_CODEC_ERROR;
+      mIndex = -1;
+      mFlags = 0;
+      mOffset = 0;
+      mPresentationTimeMicroseconds = 0;
+      mNumBytes = 0;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private DequeueOutputResult(
+        int status,
+        int index,
+        int flags,
+        int offset,
+        long presentationTimeMicroseconds,
+        int numBytes) {
+      mStatus = status;
+      mIndex = index;
+      mFlags = flags;
+      mOffset = offset;
+      mPresentationTimeMicroseconds = presentationTimeMicroseconds;
+      mNumBytes = numBytes;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int status() {
+      return mStatus;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int index() {
+      return mIndex;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int flags() {
+      return mFlags;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int offset() {
+      return mOffset;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private long presentationTimeMicroseconds() {
+      return mPresentationTimeMicroseconds;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int numBytes() {
+      return mNumBytes;
+    }
+  }
+
+  /** A wrapper around a MediaFormat. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private static class GetOutputFormatResult {
+    private int mStatus;
+    // May be null if mStatus is not MEDIA_CODEC_OK.
+    private MediaFormat mFormat;
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private GetOutputFormatResult() {
+      mStatus = MEDIA_CODEC_ERROR;
+      mFormat = null;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private GetOutputFormatResult(int status, MediaFormat format) {
+      mStatus = status;
+      mFormat = format;
+    }
+
+    private boolean formatHasCropValues() {
+      return mFormat.containsKey(KEY_CROP_RIGHT)
+          && mFormat.containsKey(KEY_CROP_LEFT)
+          && mFormat.containsKey(KEY_CROP_BOTTOM)
+          && mFormat.containsKey(KEY_CROP_TOP);
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int status() {
+      return mStatus;
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int width() {
+      return formatHasCropValues()
+          ? mFormat.getInteger(KEY_CROP_RIGHT) - mFormat.getInteger(KEY_CROP_LEFT) + 1
+          : mFormat.getInteger(MediaFormat.KEY_WIDTH);
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int height() {
+      return formatHasCropValues()
+          ? mFormat.getInteger(KEY_CROP_BOTTOM) - mFormat.getInteger(KEY_CROP_TOP) + 1
+          : mFormat.getInteger(MediaFormat.KEY_HEIGHT);
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int sampleRate() {
+      return mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+    }
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    private int channelCount() {
+      return mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private static class ColorInfo {
+    private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3.
+    private static final int DEFAULT_MAX_CLL = 1000;
+    private static final int DEFAULT_MAX_FALL = 200;
+
+    public int colorRange;
+    public int colorStandard;
+    public int colorTransfer;
+    public ByteBuffer hdrStaticInfo;
+
+    @SuppressWarnings("unused")
+    @UsedByNative
+    ColorInfo(
+        int colorRange,
+        int colorStandard,
+        int colorTransfer,
+        float primaryRChromaticityX,
+        float primaryRChromaticityY,
+        float primaryGChromaticityX,
+        float primaryGChromaticityY,
+        float primaryBChromaticityX,
+        float primaryBChromaticityY,
+        float whitePointChromaticityX,
+        float whitePointChromaticityY,
+        float maxMasteringLuminance,
+        float minMasteringLuminance) {
+      this.colorRange = colorRange;
+      this.colorStandard = colorStandard;
+      this.colorTransfer = colorTransfer;
+
+      // This logic is inspired by
+      // https://github.com/google/ExoPlayer/blob/deb9b301b2c7ef66fdd7d8a3e58298a79ba9c619/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java#L1803.
+      byte[] hdrStaticInfoData = new byte[25];
+      ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData);
+      hdrStaticInfo.put((byte) 0);
+      hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));
+      hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));
+      hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));
+      hdrStaticInfo.putShort((short) DEFAULT_MAX_CLL);
+      hdrStaticInfo.putShort((short) DEFAULT_MAX_FALL);
+      this.hdrStaticInfo = hdrStaticInfo;
+    }
+  }
+
+  private MediaCodecBridge(
+      MediaCodec mediaCodec,
+      String mime,
+      boolean adaptivePlaybackSupported,
+      BitrateAdjustmentTypes bitrateAdjustmentType) {
+    if (mediaCodec == null) {
+      throw new IllegalArgumentException();
+    }
+    mMediaCodec = mediaCodec;
+    mMime = mime; // TODO: Delete the unused mMime field
+    mLastPresentationTimeUs = 0;
+    mFlushed = true;
+    mAdaptivePlaybackSupported = adaptivePlaybackSupported;
+    mBitrateAdjustmentType = bitrateAdjustmentType;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static MediaCodecBridge createAudioMediaCodecBridge(
+      String mime,
+      boolean isSecure,
+      boolean requireSoftwareCodec,
+      int sampleRate,
+      int channelCount,
+      MediaCrypto crypto) {
+    MediaCodec mediaCodec = null;
+    try {
+      String decoderName = MediaCodecUtil.findAudioDecoder(mime, 0);
+      if (decoderName.equals("")) {
+        Log.e(TAG, String.format("Failed to find decoder: %s, isSecure: %s", mime, isSecure));
+        return null;
+      }
+      Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+      mediaCodec = MediaCodec.createByCodecName(decoderName);
+    } catch (Exception e) {
+      Log.e(TAG, String.format("Failed to create MediaCodec: %s, isSecure: %s", mime, isSecure), e);
+      return null;
+    }
+    if (mediaCodec == null) {
+      return null;
+    }
+    MediaCodecBridge bridge =
+        new MediaCodecBridge(mediaCodec, mime, true, BitrateAdjustmentTypes.NO_ADJUSTMENT);
+
+    MediaFormat mediaFormat = createAudioFormat(mime, sampleRate, channelCount);
+    setFrameHasADTSHeader(mediaFormat);
+    if (!bridge.configureAudio(mediaFormat, crypto, 0)) {
+      Log.e(TAG, "Failed to configure audio codec.");
+      bridge.release();
+      return null;
+    }
+    if (!bridge.start()) {
+      Log.e(TAG, "Failed to start audio codec.");
+      bridge.release();
+      return null;
+    }
+
+    return bridge;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static MediaCodecBridge createVideoMediaCodecBridge(
+      String mime,
+      boolean isSecure,
+      boolean requireSoftwareCodec,
+      int width,
+      int height,
+      Surface surface,
+      MediaCrypto crypto,
+      ColorInfo colorInfo) {
+    MediaCodec mediaCodec = null;
+
+    boolean findHDRDecoder = android.os.Build.VERSION.SDK_INT >= 24 && colorInfo != null;
+    // On first pass, try to find a decoder with HDR if the color info is non-null.
+    MediaCodecUtil.FindVideoDecoderResult findVideoDecoderResult =
+        MediaCodecUtil.findVideoDecoder(mime, isSecure, 0, 0, 0, 0, findHDRDecoder);
+    if (findVideoDecoderResult.name.equals("") && findHDRDecoder) {
+      // On second pass, forget HDR.
+      findVideoDecoderResult = MediaCodecUtil.findVideoDecoder(mime, isSecure, 0, 0, 0, 0, false);
+    }
+    try {
+      String decoderName = findVideoDecoderResult.name;
+      if (decoderName.equals("") || findVideoDecoderResult.videoCapabilities == null) {
+        Log.e(TAG, String.format("Failed to find decoder: %s, isSecure: %s", mime, isSecure));
+        return null;
+      }
+      Log.i(TAG, String.format("Creating \"%s\" decoder.", decoderName));
+      mediaCodec = MediaCodec.createByCodecName(decoderName);
+    } catch (Exception e) {
+      Log.e(TAG, String.format("Failed to create MediaCodec: %s, isSecure: %s", mime, isSecure), e);
+      return null;
+    }
+    if (mediaCodec == null) {
+      return null;
+    }
+    MediaCodecBridge bridge =
+        new MediaCodecBridge(mediaCodec, mime, true, BitrateAdjustmentTypes.NO_ADJUSTMENT);
+    MediaFormat mediaFormat =
+        createVideoDecoderFormat(mime, width, height, findVideoDecoderResult.videoCapabilities);
+
+    boolean shouldConfigureHdr =
+        android.os.Build.VERSION.SDK_INT >= 24
+            && colorInfo != null
+            && MediaCodecUtil.isHdrCapableVp9Decoder(findVideoDecoderResult);
+    if (shouldConfigureHdr) {
+      Log.d(TAG, "Setting HDR info.");
+      mediaFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
+      mediaFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorStandard);
+      mediaFormat.setInteger(MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
+      mediaFormat.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);
+    }
+
+    int maxWidth = findVideoDecoderResult.videoCapabilities.getSupportedWidths().getUpper();
+    int maxHeight = findVideoDecoderResult.videoCapabilities.getSupportedHeights().getUpper();
+    if (!bridge.configureVideo(mediaFormat, surface, crypto, 0, true, maxWidth, maxHeight)) {
+      Log.e(TAG, "Failed to configure video codec.");
+      bridge.release();
+      return null;
+    }
+    if (!bridge.start()) {
+      Log.e(TAG, "Failed to start video codec.");
+      bridge.release();
+      return null;
+    }
+
+    return bridge;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void release() {
+    try {
+      String codecName = mMediaCodec.getName();
+      Log.w(TAG, "calling MediaCodec.release() on " + codecName);
+      mMediaCodec.release();
+    } catch (IllegalStateException e) {
+      // The MediaCodec is stuck in a wrong state, possibly due to losing
+      // the surface.
+      Log.e(TAG, "Cannot release media codec", e);
+    }
+    mMediaCodec = null;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private boolean start() {
+    try {
+      mMediaCodec.start();
+    } catch (IllegalStateException | IllegalArgumentException e) {
+      Log.e(TAG, "Cannot start the media codec", e);
+      return false;
+    }
+    return true;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void dequeueInputBuffer(long timeoutUs, DequeueInputResult outDequeueInputResult) {
+    int status = MEDIA_CODEC_ERROR;
+    int index = -1;
+    try {
+      int indexOrStatus = mMediaCodec.dequeueInputBuffer(timeoutUs);
+      if (indexOrStatus >= 0) { // index!
+        status = MEDIA_CODEC_OK;
+        index = indexOrStatus;
+      } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+        status = MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER;
+      } else {
+        throw new AssertionError("Unexpected index_or_status: " + indexOrStatus);
+      }
+    } catch (Exception e) {
+      Log.e(TAG, "Failed to dequeue input buffer", e);
+    }
+    outDequeueInputResult.mStatus = status;
+    outDequeueInputResult.mIndex = index;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private int flush() {
+    try {
+      mFlushed = true;
+      mMediaCodec.flush();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to flush MediaCodec", e);
+      return MEDIA_CODEC_ERROR;
+    }
+    return MEDIA_CODEC_OK;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void stop() {
+    try {
+      mMediaCodec.stop();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to stop MediaCodec", e);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private String getName() {
+    String codecName = "unknown";
+    try {
+      codecName = mMediaCodec.getName();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Cannot get codec name", e);
+    }
+    return codecName;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void getOutputFormat(GetOutputFormatResult outGetOutputFormatResult) {
+    MediaFormat format = null;
+    int status = MEDIA_CODEC_OK;
+    try {
+      format = mMediaCodec.getOutputFormat();
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to get output format", e);
+      status = MEDIA_CODEC_ERROR;
+    }
+    outGetOutputFormatResult.mStatus = status;
+    outGetOutputFormatResult.mFormat = format;
+  }
+
+  /** Returns null if MediaCodec throws IllegalStateException. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private ByteBuffer getInputBuffer(int index) {
+    try {
+      return mMediaCodec.getInputBuffer(index);
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to get input buffer", e);
+      return null;
+    }
+  }
+
+  /** Returns null if MediaCodec throws IllegalStateException. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private ByteBuffer getOutputBuffer(int index) {
+    try {
+      return mMediaCodec.getOutputBuffer(index);
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to get output buffer", e);
+      return null;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private int queueInputBuffer(
+      int index, int offset, int size, long presentationTimeUs, int flags) {
+    resetLastPresentationTimeIfNeeded(presentationTimeUs);
+    try {
+      mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
+    } catch (Exception e) {
+      Log.e(TAG, "Failed to queue input buffer", e);
+      return MEDIA_CODEC_ERROR;
+    }
+    return MEDIA_CODEC_OK;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void setVideoBitrate(int bps, int frameRate) {
+    int targetBps = bps;
+    if (mBitrateAdjustmentType == BitrateAdjustmentTypes.FRAMERATE_ADJUSTMENT && frameRate > 0) {
+      targetBps = BITRATE_ADJUSTMENT_FPS * bps / frameRate;
+    }
+
+    Bundle b = new Bundle();
+    b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, targetBps);
+    try {
+      mMediaCodec.setParameters(b);
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to set MediaCodec parameters", e);
+    }
+    Log.v(TAG, "setVideoBitrate: input " + bps + "bps@" + frameRate + ", targetBps " + targetBps);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void requestKeyFrameSoon() {
+    Bundle b = new Bundle();
+    b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+    try {
+      mMediaCodec.setParameters(b);
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to set MediaCodec parameters", e);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private int queueSecureInputBuffer(
+      int index,
+      int offset,
+      byte[] iv,
+      byte[] keyId,
+      int[] numBytesOfClearData,
+      int[] numBytesOfEncryptedData,
+      int numSubSamples,
+      int cipherMode,
+      int patternEncrypt,
+      int patternSkip,
+      long presentationTimeUs) {
+    resetLastPresentationTimeIfNeeded(presentationTimeUs);
+    try {
+      boolean usesCbcs =
+          Build.VERSION.SDK_INT >= 24 && cipherMode == MediaCodec.CRYPTO_MODE_AES_CBC;
+
+      if (usesCbcs) {
+        Log.e(TAG, "Encryption scheme 'cbcs' not supported on this platform.");
+        return MEDIA_CODEC_ERROR;
+      }
+      CryptoInfo cryptoInfo = new CryptoInfo();
+      cryptoInfo.set(
+          numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, keyId, iv, cipherMode);
+      if (patternEncrypt != 0 && patternSkip != 0) {
+        if (usesCbcs) {
+          // Above platform check ensured that setting the pattern is indeed supported.
+          // MediaCodecUtil.setPatternIfSupported(cryptoInfo, patternEncrypt, patternSkip);
+          Log.e(TAG, "Only AES_CTR is supported.");
+        } else {
+          Log.e(TAG, "Pattern encryption only supported for 'cbcs' scheme (CBC mode).");
+          return MEDIA_CODEC_ERROR;
+        }
+      }
+      mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0);
+    } catch (MediaCodec.CryptoException e) {
+      int errorCode = e.getErrorCode();
+      if (errorCode == MediaCodec.CryptoException.ERROR_NO_KEY) {
+        Log.d(TAG, "Failed to queue secure input buffer: CryptoException.ERROR_NO_KEY");
+        return MEDIA_CODEC_NO_KEY;
+      } else if (errorCode == MediaCodec.CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION) {
+        Log.d(
+            TAG,
+            "Failed to queue secure input buffer: "
+                + "CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION");
+        // Note that in Android OS version before 23, the MediaDrm class doesn't expose the current
+        // key ids it holds.  In such case the Starboard media stack is unable to notify Cobalt of
+        // the error via key statuses so MEDIA_CODEC_ERROR is returned instead to signal a general
+        // media codec error.
+        return Build.VERSION.SDK_INT >= 23
+            ? MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION
+            : MEDIA_CODEC_ERROR;
+      }
+      Log.e(
+          TAG,
+          "Failed to queue secure input buffer, CryptoException with error code "
+              + e.getErrorCode());
+      return MEDIA_CODEC_ERROR;
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to queue secure input buffer, IllegalStateException " + e);
+      return MEDIA_CODEC_ERROR;
+    }
+    return MEDIA_CODEC_OK;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void releaseOutputBuffer(int index, boolean render) {
+    try {
+      mMediaCodec.releaseOutputBuffer(index, render);
+    } catch (IllegalStateException e) {
+      // TODO: May need to report the error to the caller. crbug.com/356498.
+      Log.e(TAG, "Failed to release output buffer", e);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private void releaseOutputBuffer(int index, long renderTimestampNs) {
+    try {
+      mMediaCodec.releaseOutputBuffer(index, renderTimestampNs);
+    } catch (IllegalStateException e) {
+      // TODO: May need to report the error to the caller. crbug.com/356498.
+      Log.e(TAG, "Failed to release output buffer", e);
+    }
+  }
+
+  @SuppressWarnings({"unused", "deprecation"})
+  @UsedByNative
+  private void dequeueOutputBuffer(long timeoutUs, DequeueOutputResult outDequeueOutputResult) {
+    int status = MEDIA_CODEC_ERROR;
+    int index = -1;
+    try {
+      int indexOrStatus = mMediaCodec.dequeueOutputBuffer(info, timeoutUs);
+      if (info.presentationTimeUs < mLastPresentationTimeUs) {
+        // TODO: return a special code through DequeueOutputResult
+        // to notify the native code that the frame has a wrong presentation
+        // timestamp and should be skipped.
+        info.presentationTimeUs = mLastPresentationTimeUs;
+      }
+      mLastPresentationTimeUs = info.presentationTimeUs;
+
+      if (indexOrStatus >= 0) { // index!
+        status = MEDIA_CODEC_OK;
+        index = indexOrStatus;
+      } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+        status = MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED;
+      } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+        status = MEDIA_CODEC_OUTPUT_FORMAT_CHANGED;
+        MediaFormat newFormat = mMediaCodec.getOutputFormat();
+      } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+        status = MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER;
+      } else {
+        throw new AssertionError("Unexpected index_or_status: " + indexOrStatus);
+      }
+    } catch (IllegalStateException e) {
+      status = MEDIA_CODEC_ERROR;
+      Log.e(TAG, "Failed to dequeue output buffer", e);
+    }
+
+    outDequeueOutputResult.mStatus = status;
+    outDequeueOutputResult.mIndex = index;
+    outDequeueOutputResult.mFlags = info.flags;
+    outDequeueOutputResult.mOffset = info.offset;
+    outDequeueOutputResult.mPresentationTimeMicroseconds = info.presentationTimeUs;
+    outDequeueOutputResult.mNumBytes = info.size;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private boolean configureVideo(
+      MediaFormat format,
+      Surface surface,
+      MediaCrypto crypto,
+      int flags,
+      boolean allowAdaptivePlayback,
+      int maxSupportedWidth,
+      int maxSupportedHeight) {
+    try {
+      // If adaptive playback is turned off by request, then treat it as
+      // not supported.  Note that configureVideo is only called once
+      // during creation, else this would prevent re-enabling adaptive
+      // playback later.
+      if (!allowAdaptivePlayback) {
+        mAdaptivePlaybackSupported = false;
+      }
+
+      if (mAdaptivePlaybackSupported) {
+        // Since we haven't passed the properties of the stream we're playing
+        // down to this level, from our perspective, we could potentially
+        // adapt up to 4k at any point.  We thus request 4k buffers up front,
+        // unless the decoder claims to not be able to do 4k, in which case
+        // we're ok, since we would've rejected a 4k stream when canPlayType
+        // was called, and then use those decoder values instead.
+        int maxWidth = Math.min(3840, maxSupportedWidth);
+        int maxHeight = Math.min(2160, maxSupportedHeight);
+        format.setInteger(MediaFormat.KEY_MAX_WIDTH, maxWidth);
+        format.setInteger(MediaFormat.KEY_MAX_HEIGHT, maxHeight);
+      }
+      maybeSetMaxInputSize(format);
+      mMediaCodec.configure(format, surface, crypto, flags);
+      return true;
+    } catch (IllegalArgumentException e) {
+      Log.e(TAG, "Cannot configure the video codec, wrong format or surface", e);
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Cannot configure the video codec", e);
+    } catch (MediaCodec.CryptoException e) {
+      Log.e(TAG, "Cannot configure the video codec: DRM error", e);
+    } catch (Exception e) {
+      Log.e(TAG, "Cannot configure the video codec", e);
+    }
+    return false;
+  }
+
+  public static MediaFormat createAudioFormat(String mime, int sampleRate, int channelCount) {
+    return MediaFormat.createAudioFormat(mime, sampleRate, channelCount);
+  }
+
+  private static MediaFormat createVideoDecoderFormat(
+      String mime, int width, int height, VideoCapabilities videoCapabilities) {
+    return MediaFormat.createVideoFormat(
+        mime,
+        alignDimension(width, videoCapabilities.getWidthAlignment()),
+        alignDimension(height, videoCapabilities.getHeightAlignment()));
+  }
+
+  private static int alignDimension(int size, int alignment) {
+    int ceilDivide = (size + alignment - 1) / alignment;
+    return ceilDivide * alignment;
+  }
+
+  // Use some heuristics to set KEY_MAX_INPUT_SIZE (the size of the input buffers).
+  // Taken from exoplayer:
+  // https://github.com/google/ExoPlayer/blob/8595c65678a181296cdf673eacb93d8135479340/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
+  private void maybeSetMaxInputSize(MediaFormat format) {
+    if (format.containsKey(android.media.MediaFormat.KEY_MAX_INPUT_SIZE)) {
+      // Already set. The source of the format may know better, so do nothing.
+      return;
+    }
+    int maxHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
+    if (mAdaptivePlaybackSupported && format.containsKey(MediaFormat.KEY_MAX_HEIGHT)) {
+      maxHeight = Math.max(maxHeight, format.getInteger(MediaFormat.KEY_MAX_HEIGHT));
+    }
+    int maxWidth = format.getInteger(MediaFormat.KEY_WIDTH);
+    if (mAdaptivePlaybackSupported && format.containsKey(MediaFormat.KEY_MAX_WIDTH)) {
+      maxWidth = Math.max(maxHeight, format.getInteger(MediaFormat.KEY_MAX_WIDTH));
+    }
+    int maxPixels;
+    int minCompressionRatio;
+    switch (format.getString(MediaFormat.KEY_MIME)) {
+      case MimeTypes.VIDEO_H264:
+        if ("BRAVIA 4K 2015".equals(Build.MODEL)) {
+          // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated
+          // 4k video maximum input size, so use the default value.
+          return;
+        }
+        // Round up width/height to an integer number of macroblocks.
+        maxPixels = ((maxWidth + 15) / 16) * ((maxHeight + 15) / 16) * 16 * 16;
+        minCompressionRatio = 2;
+        break;
+      case MimeTypes.VIDEO_VP8:
+        // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
+        maxPixels = maxWidth * maxHeight;
+        minCompressionRatio = 2;
+        break;
+      case MimeTypes.VIDEO_H265:
+      case MimeTypes.VIDEO_VP9:
+        maxPixels = maxWidth * maxHeight;
+        minCompressionRatio = 4;
+        break;
+      default:
+        // Leave the default max input size.
+        return;
+    }
+    // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
+    int maxInputSize = (maxPixels * 3) / (2 * minCompressionRatio);
+    format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private boolean isAdaptivePlaybackSupported(int width, int height) {
+    // If media codec has adaptive playback supported, then the max sizes
+    // used during creation are only hints.
+    return mAdaptivePlaybackSupported;
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private static void setCodecSpecificData(MediaFormat format, int index, byte[] bytes) {
+    // Codec Specific Data is set in the MediaFormat as ByteBuffer entries with keys csd-0,
+    // csd-1, and so on. See: http://developer.android.com/reference/android/media/MediaCodec.html
+    // for details.
+    String name;
+    switch (index) {
+      case 0:
+        name = "csd-0";
+        break;
+      case 1:
+        name = "csd-1";
+        break;
+      case 2:
+        name = "csd-2";
+        break;
+      default:
+        name = null;
+        break;
+    }
+    if (name != null) {
+      format.setByteBuffer(name, ByteBuffer.wrap(bytes));
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private static void setFrameHasADTSHeader(MediaFormat format) {
+    format.setInteger(MediaFormat.KEY_IS_ADTS, 1);
+  }
+
+  @SuppressWarnings("unused")
+  @UsedByNative
+  private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags) {
+    try {
+      mMediaCodec.configure(format, null, crypto, flags);
+      return true;
+    } catch (IllegalArgumentException | IllegalStateException e) {
+      Log.e(TAG, "Cannot configure the audio codec", e);
+    } catch (MediaCodec.CryptoException e) {
+      Log.e(TAG, "Cannot configure the audio codec: DRM error", e);
+    } catch (Exception e) {
+      Log.e(TAG, "Cannot configure the audio codec", e);
+    }
+    return false;
+  }
+
+  private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) {
+    if (mFlushed) {
+      mLastPresentationTimeUs =
+          Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0);
+      mFlushed = false;
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  private int getAudioFormat(int channelCount) {
+    switch (channelCount) {
+      case 1:
+        return AudioFormat.CHANNEL_OUT_MONO;
+      case 2:
+        return AudioFormat.CHANNEL_OUT_STEREO;
+      case 4:
+        return AudioFormat.CHANNEL_OUT_QUAD;
+      case 6:
+        return AudioFormat.CHANNEL_OUT_5POINT1;
+      case 8:
+        if (Build.VERSION.SDK_INT >= 23) {
+          return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+        } else {
+          return AudioFormat.CHANNEL_OUT_7POINT1;
+        }
+      default:
+        return AudioFormat.CHANNEL_OUT_DEFAULT;
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
new file mode 100644
index 0000000..25a13c2
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -0,0 +1,658 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.AudioCapabilities;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCodecList;
+import android.os.Build;
+import dev.cobalt.util.IsEmulator;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Utility functions for dealing with MediaCodec related things. */
+public class MediaCodecUtil {
+  // A low priority black list of codec names that should never be used.
+  private static final Set<String> codecBlackList = new HashSet<>();
+  // A high priority white list of brands/model that should always attempt to
+  // play vp9.
+  private static final Map<String, Set<String>> vp9WhiteList = new HashMap<>();
+  // Whether we should report vp9 codecs as supported or not.  Will be set
+  // based on whether vp9WhiteList contains our brand/model.  If this is set
+  // to true, then codecBlackList will be ignored.
+  private static boolean isVp9WhiteListed;
+  private static final String SECURE_DECODER_SUFFIX = ".secure";
+  private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+
+  /**
+   * A simple "struct" to bundle up the results from findVideoDecoder, as its clients may require
+   * the max supported width and height in addition to just the decoder name.
+   */
+  public static final class FindVideoDecoderResult {
+    public String name;
+    public VideoCapabilities videoCapabilities;
+    public CodecCapabilities codecCapabilities;
+
+    public FindVideoDecoderResult(
+        String name, VideoCapabilities videoCapabilities, CodecCapabilities codecCapabilities) {
+      this.name = name;
+      this.videoCapabilities = videoCapabilities;
+      this.codecCapabilities = codecCapabilities;
+    }
+  }
+
+  static {
+    if (Build.VERSION.SDK_INT >= 24 && Build.BRAND.equals("google")) {
+      codecBlackList.add("OMX.Nvidia.vp9.decode");
+    }
+    if (Build.VERSION.SDK_INT >= 24 && Build.BRAND.equals("LGE")) {
+      codecBlackList.add("OMX.qcom.video.decoder.vp9");
+    }
+    if (Build.VERSION.RELEASE.startsWith("6.0.1")) {
+      codecBlackList.add("OMX.Exynos.vp9.dec");
+      codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+      codecBlackList.add("OMX.qcom.video.decoder.vp9");
+    }
+    if (Build.VERSION.RELEASE.startsWith("6.0")) {
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+      codecBlackList.add("OMX.Nvidia.vp9.decode");
+    }
+    if (Build.VERSION.RELEASE.startsWith("5.1.1")) {
+      codecBlackList.add("OMX.allwinner.video.decoder.vp9");
+      codecBlackList.add("OMX.Exynos.vp9.dec");
+      codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+      codecBlackList.add("OMX.qcom.video.decoder.vp9");
+    }
+    if (Build.VERSION.RELEASE.startsWith("5.1")) {
+      codecBlackList.add("OMX.Exynos.VP9.Decoder");
+      codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+    }
+    if (Build.VERSION.RELEASE.startsWith("5.0")) {
+      codecBlackList.add("OMX.allwinner.video.decoder.vp9");
+      codecBlackList.add("OMX.Exynos.vp9.dec");
+      codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hwr");
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.VP9");
+    }
+
+    if (Build.BRAND.equals("google")) {
+      codecBlackList.add("OMX.Intel.VideoDecoder.VP9.hybrid");
+    }
+
+    // Black list non hardware media codec names if we aren't running on an emulator.
+    if (!IsEmulator.isEmulator()) {
+      codecBlackList.add("OMX.ffmpeg.vp9.decoder");
+      codecBlackList.add("OMX.Intel.sw_vd.vp9");
+      codecBlackList.add("OMX.MTK.VIDEO.DECODER.SW.VP9");
+    }
+
+    // Black list the Google software vp9 decoder both on hardware and on the emulator.
+    // On the emulator it fails with the log: "storeMetaDataInBuffers failed w/ err -1010"
+    codecBlackList.add("OMX.google.vp9.decoder");
+
+    vp9WhiteList.put("Amlogic", new HashSet<String>());
+    vp9WhiteList.put("Arcadyan", new HashSet<String>());
+    vp9WhiteList.put("arcelik", new HashSet<String>());
+    vp9WhiteList.put("BNO", new HashSet<String>());
+    vp9WhiteList.put("BROADCOM", new HashSet<String>());
+    vp9WhiteList.put("broadcom", new HashSet<String>());
+    vp9WhiteList.put("Foxconn", new HashSet<String>());
+    vp9WhiteList.put("Freebox", new HashSet<String>());
+    vp9WhiteList.put("gfiber", new HashSet<String>());
+    vp9WhiteList.put("Google", new HashSet<String>());
+    vp9WhiteList.put("google", new HashSet<String>());
+    vp9WhiteList.put("Hisense", new HashSet<String>());
+    vp9WhiteList.put("HUAWEI", new HashSet<String>());
+    vp9WhiteList.put("KaonMedia", new HashSet<String>());
+    vp9WhiteList.put("LeTV", new HashSet<String>());
+    vp9WhiteList.put("LGE", new HashSet<String>());
+    vp9WhiteList.put("MediaTek", new HashSet<String>());
+    vp9WhiteList.put("MStar", new HashSet<String>());
+    vp9WhiteList.put("MTK", new HashSet<String>());
+    vp9WhiteList.put("NVIDIA", new HashSet<String>());
+    vp9WhiteList.put("PHILIPS", new HashSet<String>());
+    vp9WhiteList.put("Philips", new HashSet<String>());
+    vp9WhiteList.put("PIXELA CORPORATION", new HashSet<String>());
+    vp9WhiteList.put("RCA", new HashSet<String>());
+    vp9WhiteList.put("Sagemcom", new HashSet<String>());
+    vp9WhiteList.put("samsung", new HashSet<String>());
+    vp9WhiteList.put("SHARP", new HashSet<String>());
+    vp9WhiteList.put("Skyworth", new HashSet<String>());
+    vp9WhiteList.put("Sony", new HashSet<String>());
+    vp9WhiteList.put("STMicroelectronics", new HashSet<String>());
+    vp9WhiteList.put("SumitomoElectricIndustries", new HashSet<String>());
+    vp9WhiteList.put("TCL", new HashSet<String>());
+    vp9WhiteList.put("Technicolor", new HashSet<String>());
+    vp9WhiteList.put("Vestel", new HashSet<String>());
+    vp9WhiteList.put("wnc", new HashSet<String>());
+    vp9WhiteList.put("Xiaomi", new HashSet<String>());
+    vp9WhiteList.put("ZTE TV", new HashSet<String>());
+
+    vp9WhiteList.get("Amlogic").add("p212");
+    vp9WhiteList.get("Arcadyan").add("Bouygtel4K");
+    vp9WhiteList.get("Arcadyan").add("HMB2213PW22TS");
+    vp9WhiteList.get("Arcadyan").add("IPSetTopBox");
+    vp9WhiteList.get("arcelik").add("arcelik_uhd_powermax_at");
+    vp9WhiteList.get("BNO").add("QM153E");
+    vp9WhiteList.get("broadcom").add("avko");
+    vp9WhiteList.get("broadcom").add("banff");
+    vp9WhiteList.get("BROADCOM").add("BCM7XXX_TEST_SETTOP");
+    vp9WhiteList.get("broadcom").add("cypress");
+    vp9WhiteList.get("broadcom").add("dawson");
+    vp9WhiteList.get("broadcom").add("elfin");
+    vp9WhiteList.get("Foxconn").add("ba101");
+    vp9WhiteList.get("Foxconn").add("bd201");
+    vp9WhiteList.get("Freebox").add("Freebox Player Mini v2");
+    vp9WhiteList.get("gfiber").add("GFHD254");
+    vp9WhiteList.get("google").add("avko");
+    vp9WhiteList.get("google").add("marlin");
+    vp9WhiteList.get("Google").add("Pixel XL");
+    vp9WhiteList.get("Google").add("Pixel");
+    vp9WhiteList.get("google").add("sailfish");
+    vp9WhiteList.get("google").add("sprint");
+    vp9WhiteList.get("Hisense").add("HAT4KDTV");
+    vp9WhiteList.get("HUAWEI").add("X21");
+    vp9WhiteList.get("KaonMedia").add("IC1110");
+    vp9WhiteList.get("KaonMedia").add("IC1130");
+    vp9WhiteList.get("KaonMedia").add("MCM4000");
+    vp9WhiteList.get("KaonMedia").add("PRDMK100T");
+    vp9WhiteList.get("KaonMedia").add("SFCSTB2LITE");
+    vp9WhiteList.get("LeTV").add("uMax85");
+    vp9WhiteList.get("LeTV").add("X4-43Pro");
+    vp9WhiteList.get("LeTV").add("X4-55");
+    vp9WhiteList.get("LeTV").add("X4-65");
+    vp9WhiteList.get("LGE").add("S60CLI");
+    vp9WhiteList.get("LGE").add("S60UPA");
+    vp9WhiteList.get("LGE").add("S60UPI");
+    vp9WhiteList.get("LGE").add("S70CDS");
+    vp9WhiteList.get("LGE").add("S70PCI");
+    vp9WhiteList.get("LGE").add("SH960C-DS");
+    vp9WhiteList.get("LGE").add("SH960C-LN");
+    vp9WhiteList.get("LGE").add("SH960S-AT");
+    vp9WhiteList.get("MediaTek").add("Archer");
+    vp9WhiteList.get("MediaTek").add("augie");
+    vp9WhiteList.get("MediaTek").add("kane");
+    vp9WhiteList.get("MStar").add("Denali");
+    vp9WhiteList.get("MStar").add("Rainier");
+    vp9WhiteList.get("MTK").add("Generic Android on sharp_2k15_us_android");
+    vp9WhiteList.get("NVIDIA").add("SHIELD Android TV");
+    vp9WhiteList.get("NVIDIA").add("SHIELD Console");
+    vp9WhiteList.get("NVIDIA").add("SHIELD Portable");
+    vp9WhiteList.get("PHILIPS").add("QM151E");
+    vp9WhiteList.get("PHILIPS").add("QM161E");
+    vp9WhiteList.get("PHILIPS").add("QM163E");
+    vp9WhiteList.get("Philips").add("TPM171E");
+    vp9WhiteList.get("PIXELA CORPORATION").add("POE-MP4000");
+    vp9WhiteList.get("RCA").add("XLDRCAV1");
+    vp9WhiteList.get("Sagemcom").add("DNA Android TV");
+    vp9WhiteList.get("Sagemcom").add("GigaTV");
+    vp9WhiteList.get("Sagemcom").add("M387_QL");
+    vp9WhiteList.get("Sagemcom").add("Sagemcom Android STB");
+    vp9WhiteList.get("Sagemcom").add("Sagemcom ATV Demo");
+    vp9WhiteList.get("Sagemcom").add("Telecable ATV");
+    vp9WhiteList.get("samsung").add("c71kw200");
+    vp9WhiteList.get("samsung").add("GX-CJ680CL");
+    vp9WhiteList.get("samsung").add("SAMSUNG-SM-G890A");
+    vp9WhiteList.get("samsung").add("SAMSUNG-SM-G920A");
+    vp9WhiteList.get("samsung").add("SAMSUNG-SM-G920AZ");
+    vp9WhiteList.get("samsung").add("SAMSUNG-SM-G925A");
+    vp9WhiteList.get("samsung").add("SAMSUNG-SM-G928A");
+    vp9WhiteList.get("samsung").add("SM-G9200");
+    vp9WhiteList.get("samsung").add("SM-G9208");
+    vp9WhiteList.get("samsung").add("SM-G9209");
+    vp9WhiteList.get("samsung").add("SM-G920A");
+    vp9WhiteList.get("samsung").add("SM-G920D");
+    vp9WhiteList.get("samsung").add("SM-G920F");
+    vp9WhiteList.get("samsung").add("SM-G920FD");
+    vp9WhiteList.get("samsung").add("SM-G920FQ");
+    vp9WhiteList.get("samsung").add("SM-G920I");
+    vp9WhiteList.get("samsung").add("SM-G920K");
+    vp9WhiteList.get("samsung").add("SM-G920L");
+    vp9WhiteList.get("samsung").add("SM-G920P");
+    vp9WhiteList.get("samsung").add("SM-G920R4");
+    vp9WhiteList.get("samsung").add("SM-G920R6");
+    vp9WhiteList.get("samsung").add("SM-G920R7");
+    vp9WhiteList.get("samsung").add("SM-G920S");
+    vp9WhiteList.get("samsung").add("SM-G920T");
+    vp9WhiteList.get("samsung").add("SM-G920T1");
+    vp9WhiteList.get("samsung").add("SM-G920V");
+    vp9WhiteList.get("samsung").add("SM-G920W8");
+    vp9WhiteList.get("samsung").add("SM-G9250");
+    vp9WhiteList.get("samsung").add("SM-G925A");
+    vp9WhiteList.get("samsung").add("SM-G925D");
+    vp9WhiteList.get("samsung").add("SM-G925F");
+    vp9WhiteList.get("samsung").add("SM-G925FQ");
+    vp9WhiteList.get("samsung").add("SM-G925I");
+    vp9WhiteList.get("samsung").add("SM-G925J");
+    vp9WhiteList.get("samsung").add("SM-G925K");
+    vp9WhiteList.get("samsung").add("SM-G925L");
+    vp9WhiteList.get("samsung").add("SM-G925P");
+    vp9WhiteList.get("samsung").add("SM-G925R4");
+    vp9WhiteList.get("samsung").add("SM-G925R6");
+    vp9WhiteList.get("samsung").add("SM-G925R7");
+    vp9WhiteList.get("samsung").add("SM-G925S");
+    vp9WhiteList.get("samsung").add("SM-G925T");
+    vp9WhiteList.get("samsung").add("SM-G925V");
+    vp9WhiteList.get("samsung").add("SM-G925W8");
+    vp9WhiteList.get("samsung").add("SM-G925Z");
+    vp9WhiteList.get("samsung").add("SM-G9280");
+    vp9WhiteList.get("samsung").add("SM-G9287");
+    vp9WhiteList.get("samsung").add("SM-G9287C");
+    vp9WhiteList.get("samsung").add("SM-G928A");
+    vp9WhiteList.get("samsung").add("SM-G928C");
+    vp9WhiteList.get("samsung").add("SM-G928F");
+    vp9WhiteList.get("samsung").add("SM-G928G");
+    vp9WhiteList.get("samsung").add("SM-G928I");
+    vp9WhiteList.get("samsung").add("SM-G928K");
+    vp9WhiteList.get("samsung").add("SM-G928L");
+    vp9WhiteList.get("samsung").add("SM-G928N0");
+    vp9WhiteList.get("samsung").add("SM-G928P");
+    vp9WhiteList.get("samsung").add("SM-G928S");
+    vp9WhiteList.get("samsung").add("SM-G928T");
+    vp9WhiteList.get("samsung").add("SM-G928V");
+    vp9WhiteList.get("samsung").add("SM-G928W8");
+    vp9WhiteList.get("samsung").add("SM-G928X");
+    vp9WhiteList.get("samsung").add("SM-G9300");
+    vp9WhiteList.get("samsung").add("SM-G9308");
+    vp9WhiteList.get("samsung").add("SM-G930A");
+    vp9WhiteList.get("samsung").add("SM-G930AZ");
+    vp9WhiteList.get("samsung").add("SM-G930F");
+    vp9WhiteList.get("samsung").add("SM-G930FD");
+    vp9WhiteList.get("samsung").add("SM-G930K");
+    vp9WhiteList.get("samsung").add("SM-G930L");
+    vp9WhiteList.get("samsung").add("SM-G930P");
+    vp9WhiteList.get("samsung").add("SM-G930R4");
+    vp9WhiteList.get("samsung").add("SM-G930R6");
+    vp9WhiteList.get("samsung").add("SM-G930R7");
+    vp9WhiteList.get("samsung").add("SM-G930S");
+    vp9WhiteList.get("samsung").add("SM-G930T");
+    vp9WhiteList.get("samsung").add("SM-G930T1");
+    vp9WhiteList.get("samsung").add("SM-G930U");
+    vp9WhiteList.get("samsung").add("SM-G930V");
+    vp9WhiteList.get("samsung").add("SM-G930VL");
+    vp9WhiteList.get("samsung").add("SM-G930W8");
+    vp9WhiteList.get("samsung").add("SM-G9350");
+    vp9WhiteList.get("samsung").add("SM-G935A");
+    vp9WhiteList.get("samsung").add("SM-G935D");
+    vp9WhiteList.get("samsung").add("SM-G935F");
+    vp9WhiteList.get("samsung").add("SM-G935FD");
+    vp9WhiteList.get("samsung").add("SM-G935J");
+    vp9WhiteList.get("samsung").add("SM-G935K");
+    vp9WhiteList.get("samsung").add("SM-G935L");
+    vp9WhiteList.get("samsung").add("SM-G935P");
+    vp9WhiteList.get("samsung").add("SM-G935R4");
+    vp9WhiteList.get("samsung").add("SM-G935S");
+    vp9WhiteList.get("samsung").add("SM-G935T");
+    vp9WhiteList.get("samsung").add("SM-G935U");
+    vp9WhiteList.get("samsung").add("SM-G935V");
+    vp9WhiteList.get("samsung").add("SM-G935W8");
+    vp9WhiteList.get("samsung").add("SM-N9200");
+    vp9WhiteList.get("samsung").add("SM-N9208");
+    vp9WhiteList.get("samsung").add("SM-N920A");
+    vp9WhiteList.get("samsung").add("SM-N920C");
+    vp9WhiteList.get("samsung").add("SM-N920F");
+    vp9WhiteList.get("samsung").add("SM-N920G");
+    vp9WhiteList.get("samsung").add("SM-N920I");
+    vp9WhiteList.get("samsung").add("SM-N920K");
+    vp9WhiteList.get("samsung").add("SM-N920L");
+    vp9WhiteList.get("samsung").add("SM-N920R4");
+    vp9WhiteList.get("samsung").add("SM-N920R6");
+    vp9WhiteList.get("samsung").add("SM-N920R7");
+    vp9WhiteList.get("samsung").add("SM-N920S");
+    vp9WhiteList.get("samsung").add("SM-N920T");
+    vp9WhiteList.get("samsung").add("SM-N920TP");
+    vp9WhiteList.get("samsung").add("SM-N920V");
+    vp9WhiteList.get("samsung").add("SM-N920W8");
+    vp9WhiteList.get("samsung").add("SM-N920X");
+    vp9WhiteList.get("SHARP").add("AN-NP40");
+    vp9WhiteList.get("SHARP").add("AQUOS-4KTVJ17");
+    vp9WhiteList.get("SHARP").add("AQUOS-4KTVT17");
+    vp9WhiteList.get("SHARP").add("AQUOS-4KTVX17");
+    vp9WhiteList.get("SHARP").add("LC-U35T");
+    vp9WhiteList.get("SHARP").add("LC-UE630X");
+    vp9WhiteList.get("SHARP").add("LC-Ux30US");
+    vp9WhiteList.get("SHARP").add("LC-XU35T");
+    vp9WhiteList.get("SHARP").add("LC-XU930X_830X");
+    vp9WhiteList.get("Skyworth").add("globe");
+    vp9WhiteList.get("Sony").add("Amai VP9");
+    vp9WhiteList.get("Sony").add("BRAVIA 4K 2015");
+    vp9WhiteList.get("Sony").add("BRAVIA 4K GB");
+    vp9WhiteList.get("STMicroelectronics").add("sti4k");
+    vp9WhiteList.get("SumitomoElectricIndustries").add("C02AS");
+    vp9WhiteList.get("SumitomoElectricIndustries").add("ST4173");
+    vp9WhiteList.get("SumitomoElectricIndustries").add("test_STW2000");
+    vp9WhiteList.get("TCL").add("Percee TV");
+    vp9WhiteList.get("Technicolor").add("AirTV Player");
+    vp9WhiteList.get("Technicolor").add("Bouygtel4K");
+    vp9WhiteList.get("Technicolor").add("CM-7600");
+    vp9WhiteList.get("Technicolor").add("cooper");
+    vp9WhiteList.get("Technicolor").add("Foxtel Now box");
+    vp9WhiteList.get("Technicolor").add("pearl");
+    vp9WhiteList.get("Technicolor").add("Sapphire");
+    vp9WhiteList.get("Technicolor").add("Shortcut");
+    vp9WhiteList.get("Technicolor").add("skipper");
+    vp9WhiteList.get("Technicolor").add("STING");
+    vp9WhiteList.get("Technicolor").add("TIM_BOX");
+    vp9WhiteList.get("Technicolor").add("uzx8020chm");
+    vp9WhiteList.get("Vestel").add("S7252");
+    vp9WhiteList.get("Vestel").add("SmartTV");
+    vp9WhiteList.get("wnc").add("c71kw400");
+    vp9WhiteList.get("Xiaomi").add("MIBOX3");
+    vp9WhiteList.get("ZTE TV").add("AV-ATB100");
+    vp9WhiteList.get("ZTE TV").add("B860H");
+
+    isVp9WhiteListed =
+        vp9WhiteList.containsKey(Build.BRAND)
+            && vp9WhiteList.get(Build.BRAND).contains(Build.MODEL);
+  }
+
+  private MediaCodecUtil() {}
+
+  /**
+   * Returns whether a given combination of (frame width x frame height) frames at bitrate and fps
+   * has a decoder with mime type.
+   *
+   * <p>Setting any of the int parameters to 0 indicates that they shouldn't be considered.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static boolean hasVideoDecoderFor(
+      String mimeType,
+      boolean secure,
+      int frameWidth,
+      int frameHeight,
+      int bitrate,
+      int fps,
+      boolean mustSupportHdr) {
+    FindVideoDecoderResult findVideoDecoderResult =
+        findVideoDecoder(mimeType, secure, frameWidth, frameHeight, bitrate, fps, mustSupportHdr);
+    return !findVideoDecoderResult.name.equals("")
+        && (!mustSupportHdr || isHdrCapableVp9Decoder(findVideoDecoderResult));
+  }
+
+  /**
+   * Returns whether an audio decoder that supports mimeType at bitrate. Setting bitrate to 0
+   * indicates that it should not be considered.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static boolean hasAudioDecoderFor(String mimeType, int bitrate) {
+    return !findAudioDecoder(mimeType, bitrate).equals("");
+  }
+
+  /** Determine whether the system has a decoder capable of playing HDR VP9. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public static boolean hasHdrCapableVp9Decoder() {
+    // VP9Profile* values were not added until API level 24.  See
+    // https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel.html.
+    if (Build.VERSION.SDK_INT < 24) {
+      return false;
+    }
+
+    FindVideoDecoderResult findVideoDecoderResult =
+        findVideoDecoder(VP9_MIME_TYPE, false, 0, 0, 0, 0, true);
+    return isHdrCapableVp9Decoder(findVideoDecoderResult);
+  }
+
+  /** Determine whether findVideoDecoderResult is capable of playing HDR VP9 */
+  public static boolean isHdrCapableVp9Decoder(FindVideoDecoderResult findVideoDecoderResult) {
+    CodecCapabilities codecCapabilities = findVideoDecoderResult.codecCapabilities;
+    if (codecCapabilities == null) {
+      return false;
+    }
+    CodecProfileLevel[] codecProfileLevels = codecCapabilities.profileLevels;
+    if (codecProfileLevels == null) {
+      return false;
+    }
+    for (CodecProfileLevel codecProfileLevel : codecProfileLevels) {
+      if (codecProfileLevel.profile == CodecProfileLevel.VP9Profile2HDR
+          || codecProfileLevel.profile == CodecProfileLevel.VP9Profile3HDR) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * The same as hasVideoDecoderFor, only return the name of the video decoder if it is found, and
+   * "" otherwise.
+   */
+  public static FindVideoDecoderResult findVideoDecoder(
+      String mimeType,
+      boolean secure,
+      int frameWidth,
+      int frameHeight,
+      int bitrate,
+      int fps,
+      boolean hdr) {
+    Log.v(
+        TAG,
+        String.format(
+            "Searching for video decoder with parameters "
+                + "mimeType: %s, secure: %b, frameWidth: %d, frameHeight: %d, bitrate: %d, fps: %d",
+            mimeType, secure, frameWidth, frameHeight, bitrate, fps));
+    Log.v(
+        TAG,
+        String.format(
+            "brand: %s, model: %s, version: %s, API level: %d, isVp9WhiteListed: %b",
+            Build.BRAND,
+            Build.MODEL,
+            Build.VERSION.RELEASE,
+            Build.VERSION.SDK_INT,
+            isVp9WhiteListed));
+
+    // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+    for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+      if (info.isEncoder()) {
+        continue;
+      }
+      for (String supportedType : info.getSupportedTypes()) {
+        if (!supportedType.equalsIgnoreCase(mimeType)) {
+          continue;
+        }
+
+        String name = info.getName();
+        if (!isVp9WhiteListed && codecBlackList.contains(name)) {
+          Log.v(TAG, String.format("Rejecting %s, reason: codec is black listed", name));
+          continue;
+        }
+        // MediaCodecList is supposed to feed us names of decoders that do NOT end in ".secure".  We
+        // are then supposed to check if FEATURE_SecurePlayback is supported, and it if is and we
+        // want a secure codec, we append ".secure" ourselves, and then pass that to
+        // MediaCodec.createDecoderByName.  Some devices, do not follow this spec, and show us
+        // decoders that end in ".secure".  Empirically, FEATURE_SecurePlayback has still been
+        // correct when this happens.
+        if (name.endsWith(SECURE_DECODER_SUFFIX)) {
+          // If we want a secure decoder, then make sure the version without ".secure" isn't
+          // blacklisted.
+          String nameWithoutSecureSuffix =
+              name.substring(0, name.length() - SECURE_DECODER_SUFFIX.length());
+          if (secure && !isVp9WhiteListed && codecBlackList.contains(nameWithoutSecureSuffix)) {
+            Log.v(
+                TAG,
+                String.format("Rejecting %s, reason: offpsec blacklisted secure decoder", name));
+            continue;
+          }
+          // If we don't want a secure decoder, then don't bother messing around with this thing.
+          if (!secure) {
+            Log.v(
+                TAG,
+                String.format(
+                    "Rejecting %s, reason: want !secure decoder and ends with .secure", name));
+            continue;
+          }
+        }
+
+        CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
+        if (secure
+            && !codecCapabilities.isFeatureSupported(
+                MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback)) {
+          Log.v(
+              TAG,
+              String.format(
+                  "Rejecting %s, reason: want secure decoder and !FEATURE_SecurePlayback", name));
+          continue;
+        }
+
+        // VideoCapabilties is not implemented correctly on this device.
+        if (Build.VERSION.SDK_INT < 23
+            && Build.MODEL.equals("MIBOX3")
+            && name.equals("OMX.amlogic.vp9.decoder.awesome")
+            && (frameWidth > 1920 || frameHeight > 1080)) {
+          Log.v(TAG, "Skipping >1080p OMX.amlogic.vp9.decoder.awesome on mibox.");
+          continue;
+        }
+
+        VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
+        if (frameWidth != 0 && !videoCapabilities.getSupportedWidths().contains(frameWidth)) {
+          Log.v(
+              TAG,
+              String.format(
+                  "Rejecting %s, reason: supported widths %s does not contain %d",
+                  name, videoCapabilities.getSupportedWidths().toString(), frameWidth));
+          continue;
+        }
+        if (frameHeight != 0 && !videoCapabilities.getSupportedHeights().contains(frameHeight)) {
+          Log.v(
+              TAG,
+              String.format(
+                  "Rejecting %s, reason: supported heights %s does not contain %d",
+                  name, videoCapabilities.getSupportedHeights().toString(), frameHeight));
+          continue;
+        }
+        if (bitrate != 0 && !videoCapabilities.getBitrateRange().contains(bitrate)) {
+          Log.v(
+              TAG,
+              String.format(
+                  "Rejecting %s, reason: bitrate range %s does not contain %d",
+                  name, videoCapabilities.getBitrateRange().toString(), bitrate));
+          continue;
+        }
+        if (fps != 0 && !videoCapabilities.getSupportedFrameRates().contains(fps)) {
+          Log.v(
+              TAG,
+              String.format(
+                  "Rejecting %s, reason: supported frame rates %s does not contain %d",
+                  name, videoCapabilities.getSupportedFrameRates().toString(), fps));
+          continue;
+        }
+        String resultName =
+            (secure && !name.endsWith(SECURE_DECODER_SUFFIX))
+                ? (name + SECURE_DECODER_SUFFIX)
+                : name;
+        FindVideoDecoderResult findVideoDecoderResult =
+            new FindVideoDecoderResult(resultName, videoCapabilities, codecCapabilities);
+        if (hdr && !isHdrCapableVp9Decoder(findVideoDecoderResult)) {
+          continue;
+        }
+        Log.v(TAG, String.format("Found suitable decoder, %s", name));
+        return findVideoDecoderResult;
+      }
+    }
+    return new FindVideoDecoderResult("", null, null);
+  }
+
+  /**
+   * The same as hasAudioDecoderFor, only return the name of the audio decoder if it is found, and
+   * "" otherwise.
+   */
+  public static String findAudioDecoder(String mimeType, int bitrate) {
+    // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+    for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+      if (info.isEncoder()) {
+        continue;
+      }
+      for (String supportedType : info.getSupportedTypes()) {
+        if (!supportedType.equalsIgnoreCase(mimeType)) {
+          continue;
+        }
+        String name = info.getName();
+        AudioCapabilities audioCapabilities =
+            info.getCapabilitiesForType(supportedType).getAudioCapabilities();
+        if (bitrate != 0 && !audioCapabilities.getBitrateRange().contains(bitrate)) {
+          continue;
+        }
+        return name;
+      }
+    }
+    return "";
+  }
+
+  /**
+   * Debug utility function that can be locally added to dump information about all decoders on a
+   * particular system.
+   */
+  @SuppressWarnings("unused")
+  private static void dumpAllDecoders() {
+    for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
+      if (info.isEncoder()) {
+        continue;
+      }
+      for (String supportedType : info.getSupportedTypes()) {
+        String name = info.getName();
+        CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
+        Log.v(TAG, "==================================================");
+        Log.v(TAG, String.format("name: %s", name));
+        Log.v(TAG, String.format("supportedType: %s", supportedType));
+        Log.v(
+            TAG, String.format("codecBlackList.contains(name): %b", codecBlackList.contains(name)));
+        Log.v(
+            TAG,
+            String.format(
+                "FEATURE_SecurePlayback: %b",
+                codecCapabilities.isFeatureSupported(
+                    MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback)));
+        VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
+        if (videoCapabilities != null) {
+          Log.v(
+              TAG,
+              String.format(
+                  "videoCapabilities.getSupportedWidths(): %s",
+                  videoCapabilities.getSupportedWidths().toString()));
+          Log.v(
+              TAG,
+              String.format(
+                  "videoCapabilities.getSupportedHeights(): %s",
+                  videoCapabilities.getSupportedHeights().toString()));
+          Log.v(
+              TAG,
+              String.format(
+                  "videoCapabilities.getBitrateRange(): %s",
+                  videoCapabilities.getBitrateRange().toString()));
+          Log.v(
+              TAG,
+              String.format(
+                  "videoCapabilities.getSupportedFrameRates(): %s",
+                  videoCapabilities.getSupportedFrameRates().toString()));
+        }
+        Log.v(TAG, "==================================================");
+        Log.v(TAG, "");
+      }
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
new file mode 100644
index 0000000..4619b57
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
@@ -0,0 +1,661 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrm.OnEventListener;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import dev.cobalt.coat.CobaltHttpHelper;
+import dev.cobalt.util.Log;
+import dev.cobalt.util.UsedByNative;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+/** A wrapper of the android MediaDrm class. */
+@UsedByNative
+public class MediaDrmBridge {
+  // Implementation Notes:
+  // - A media crypto session (mMediaCryptoSession) is opened after MediaDrm
+  //   is created. This session will NOT be added to mSessionIds and will only
+  //   be used to create the MediaCrypto object.
+  // - Each createSession() call creates a new session. All created sessions
+  //   are managed in mSessionIds.
+  // - Whenever NotProvisionedException is thrown, we will clean up the
+  //   current state and start the provisioning process.
+  // - When provisioning is finished, we will try to resume suspended
+  //   operations:
+  //   a) Create the media crypto session if it's not created.
+  //   b) Finish createSession() if previous createSession() was interrupted
+  //      by a NotProvisionedException.
+  // - Whenever an unexpected error occurred, we'll call release() to release
+  //   all resources immediately, clear all states and fail all pending
+  //   operations. After that all calls to this object will fail (e.g. return
+  //   null or reject the promise). All public APIs and callbacks should check
+  //   mMediaBridge to make sure release() hasn't been called.
+
+  private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();
+  private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0;
+
+  // The value of this must stay in sync with kSbDrmTicketInvalid in "starboard/drm.h"
+  private static final int SB_DRM_TICKET_INVALID = Integer.MIN_VALUE;
+
+  // Scheme UUID for Widevine. See http://dashif.org/identifiers/protection/
+  private static final UUID WIDEVINE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
+
+  // Deprecated in API 26, but we still log it on earlier devices.
+  // We do handle STATUS_EXPIRED in nativeOnKeyStatusChange() for API 23+ devices.
+  @SuppressWarnings("deprecation")
+  private static final int MEDIA_DRM_EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED;
+
+  private MediaDrm mMediaDrm;
+  private long mNativeMediaDrmBridge;
+  private UUID mSchemeUUID;
+
+  // A session only for the purpose of creating a MediaCrypto object. Created
+  // after construction, or after the provisioning process is successfully
+  // completed. No getKeyRequest() should be called on |mMediaCryptoSession|.
+  private byte[] mMediaCryptoSession;
+
+  // The map of all opened sessions (excluding mMediaCryptoSession) to their
+  // mime types.
+  private HashMap<ByteBuffer, String> mSessionIds = new HashMap<>();
+
+  private MediaCrypto mMediaCrypto;
+
+  /**
+   * Create a new MediaDrmBridge with the Widevine crypto scheme.
+   *
+   * @param nativeMediaDrmBridge The native owner of this class.
+   */
+  @UsedByNative
+  static MediaDrmBridge create(long nativeMediaDrmBridge) {
+    UUID cryptoScheme = WIDEVINE_UUID;
+    if (!MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
+      return null;
+    }
+
+    MediaDrmBridge mediaDrmBridge = null;
+    try {
+      mediaDrmBridge = new MediaDrmBridge(cryptoScheme, nativeMediaDrmBridge);
+      Log.d(TAG, "MediaDrmBridge successfully created.");
+    } catch (UnsupportedSchemeException e) {
+      Log.e(TAG, "Unsupported DRM scheme", e);
+      return null;
+    } catch (IllegalArgumentException e) {
+      Log.e(TAG, "Failed to create MediaDrmBridge", e);
+      return null;
+    } catch (IllegalStateException e) {
+      Log.e(TAG, "Failed to create MediaDrmBridge", e);
+      return null;
+    }
+
+    if (!mediaDrmBridge.createMediaCrypto()) {
+      return null;
+    }
+
+    return mediaDrmBridge;
+  }
+
+  /**
+   * Check whether the Widevine crypto scheme is supported.
+   *
+   * @return true if the container and the crypto scheme is supported, or false otherwise.
+   */
+  @UsedByNative
+  static boolean isWidevineCryptoSchemeSupported() {
+    return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
+  }
+
+  /**
+   * Check whether the Widevine crypto scheme is supported for the given container. If
+   * |containerMimeType| is an empty string, we just return whether the crypto scheme is supported.
+   *
+   * @return true if the container and the crypto scheme is supported, or false otherwise.
+   */
+  @UsedByNative
+  static boolean isWidevineCryptoSchemeSupported(String containerMimeType) {
+    if (containerMimeType.isEmpty()) {
+      return isWidevineCryptoSchemeSupported();
+    }
+    return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID, containerMimeType);
+  }
+
+  /** Destroy the MediaDrmBridge object. */
+  @UsedByNative
+  void destroy() {
+    mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+    if (mMediaDrm != null) {
+      release();
+    }
+  }
+
+  @UsedByNative
+  void createSession(int ticket, byte[] initData, String mime) {
+    Log.d(TAG, "createSession()");
+
+    if (mMediaDrm == null) {
+      Log.e(TAG, "createSession() called when MediaDrm is null.");
+      return;
+    }
+
+    boolean newSessionOpened = false;
+    byte[] sessionId = null;
+    try {
+      sessionId = openSession();
+      if (sessionId == null) {
+        Log.e(TAG, "Open session failed.");
+        return;
+      }
+      newSessionOpened = true;
+      if (sessionExists(sessionId)) {
+        Log.e(TAG, "Opened session that already exists.");
+        return;
+      }
+
+      MediaDrm.KeyRequest request = null;
+      request = getKeyRequest(sessionId, initData, mime);
+      if (request == null) {
+        try {
+          // Some implementations let this method throw exceptions.
+          mMediaDrm.closeSession(sessionId);
+        } catch (Exception e) {
+          Log.e(TAG, "closeSession failed", e);
+        }
+        Log.e(TAG, "Generate request failed.");
+        return;
+      }
+
+      // Success!
+      Log.d(
+          TAG,
+          String.format("createSession(): Session (%s) created.", bytesToHexString(sessionId)));
+      mSessionIds.put(ByteBuffer.wrap(sessionId), mime);
+      onSessionMessage(ticket, sessionId, request);
+    } catch (NotProvisionedException e) {
+      Log.e(TAG, "Device not provisioned", e);
+      if (newSessionOpened) {
+        try {
+          // Some implementations let this method throw exceptions.
+          mMediaDrm.closeSession(sessionId);
+        } catch (Exception ex) {
+          Log.e(TAG, "closeSession failed", ex);
+        }
+      }
+      attemptProvisioning();
+    }
+  }
+
+  /**
+   * Update a session with response.
+   *
+   * @param sessionId Reference ID of session to be updated.
+   * @param response Response data from the server.
+   */
+  @UsedByNative
+  boolean updateSession(byte[] sessionId, byte[] response) {
+    Log.d(TAG, "updateSession()");
+    if (mMediaDrm == null) {
+      Log.e(TAG, "updateSession() called when MediaDrm is null.");
+      return false;
+    }
+
+    if (!sessionExists(sessionId)) {
+      Log.e(TAG, "updateSession tried to update a session that does not exist.");
+      return false;
+    }
+
+    try {
+      try {
+        mMediaDrm.provideKeyResponse(sessionId, response);
+      } catch (IllegalStateException e) {
+        // This is not really an exception. Some error codes are incorrectly
+        // reported as an exception.
+        Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse()", e);
+      }
+      Log.d(
+          TAG, String.format("Key successfully added for session %s", bytesToHexString(sessionId)));
+      if (Build.VERSION.SDK_INT < 23) {
+        // Pass null to indicate that KeyStatus isn't supported.
+        nativeOnKeyStatusChange(mNativeMediaDrmBridge, sessionId, null);
+      }
+      return true;
+    } catch (NotProvisionedException e) {
+      // TODO: Should we handle this?
+      Log.e(TAG, "failed to provide key response", e);
+    } catch (DeniedByServerException e) {
+      Log.e(TAG, "failed to provide key response", e);
+    }
+    Log.e(TAG, "Update session failed.");
+    release();
+    return false;
+  }
+
+  /**
+   * Close a session that was previously created by createSession().
+   *
+   * @param sessionId ID of session to be closed.
+   */
+  @UsedByNative
+  void closeSession(byte[] sessionId) {
+    Log.d(TAG, "closeSession()");
+    if (mMediaDrm == null) {
+      Log.e(TAG, "closeSession() called when MediaDrm is null.");
+      return;
+    }
+
+    if (!sessionExists(sessionId)) {
+      Log.e(TAG, "Invalid sessionId in closeSession(): " + bytesToHexString(sessionId));
+      return;
+    }
+
+    try {
+      // Some implementations don't have removeKeys.
+      // https://bugs.chromium.org/p/chromium/issues/detail?id=475632
+      mMediaDrm.removeKeys(sessionId);
+    } catch (Exception e) {
+      Log.e(TAG, "removeKeys failed: ", e);
+    }
+    try {
+      // Some implementations let this method throw exceptions.
+      mMediaDrm.closeSession(sessionId);
+    } catch (Exception e) {
+      Log.e(TAG, "closeSession failed: ", e);
+    }
+    mSessionIds.remove(ByteBuffer.wrap(sessionId));
+    Log.d(TAG, String.format("Session %s closed", bytesToHexString(sessionId)));
+  }
+
+  @UsedByNative
+  MediaCrypto getMediaCrypto() {
+    return mMediaCrypto;
+  }
+
+  private MediaDrmBridge(UUID schemeUUID, long nativeMediaDrmBridge)
+      throws android.media.UnsupportedSchemeException {
+    mSchemeUUID = schemeUUID;
+    mMediaDrm = new MediaDrm(schemeUUID);
+
+    mNativeMediaDrmBridge = nativeMediaDrmBridge;
+    if (!isNativeMediaDrmBridgeValid()) {
+      throw new IllegalArgumentException(
+          String.format("Invalid nativeMediaDrmBridge value: |%d|.", nativeMediaDrmBridge));
+    }
+
+    mMediaDrm.setOnEventListener(
+        new OnEventListener() {
+          @Override
+          public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
+            if (sessionId == null) {
+              Log.e(TAG, "EventListener: Null session.");
+              return;
+            }
+            if (!sessionExists(sessionId)) {
+              Log.e(
+                  TAG,
+                  String.format("EventListener: Invalid session %s", bytesToHexString(sessionId)));
+              return;
+            }
+            switch (event) {
+              case MediaDrm.EVENT_KEY_REQUIRED:
+                Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED");
+                String mime = mSessionIds.get(ByteBuffer.wrap(sessionId));
+                MediaDrm.KeyRequest request = null;
+                try {
+                  request = getKeyRequest(sessionId, data, mime);
+                } catch (NotProvisionedException e) {
+                  Log.e(TAG, "Device not provisioned", e);
+                  if (!attemptProvisioning()) {
+                    Log.e(TAG, "Failed to provision device when responding to EVENT_KEY_REQUIRED");
+                    return;
+                  }
+                  // If we supposedly successfully provisioned ourselves, then try to create a
+                  // request again.
+                  try {
+                    request = getKeyRequest(sessionId, data, mime);
+                  } catch (NotProvisionedException e2) {
+                    Log.e(
+                        TAG,
+                        "Device still not provisioned after supposedly successful provisioning",
+                        e2);
+                    return;
+                  }
+                }
+                if (request != null) {
+                  onSessionMessage(SB_DRM_TICKET_INVALID, sessionId, request);
+                } else {
+                  Log.e(TAG, "EventListener: getKeyRequest failed.");
+                  return;
+                }
+                break;
+              case MEDIA_DRM_EVENT_KEY_EXPIRED:
+                Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED");
+                break;
+              case MediaDrm.EVENT_VENDOR_DEFINED:
+                Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED");
+                break;
+              default:
+                Log.e(TAG, "Invalid DRM event " + event);
+                return;
+            }
+          }
+        });
+
+    if (Build.VERSION.SDK_INT >= 23) {
+      setOnKeyStatusChangeListenerV23();
+    }
+
+    mMediaDrm.setPropertyString("privacyMode", "enable");
+    mMediaDrm.setPropertyString("sessionSharing", "enable");
+  }
+
+  @TargetApi(23)
+  private void setOnKeyStatusChangeListenerV23() {
+    mMediaDrm.setOnKeyStatusChangeListener(
+        new MediaDrm.OnKeyStatusChangeListener() {
+          @Override
+          public void onKeyStatusChange(
+              MediaDrm md,
+              byte[] sessionId,
+              List<MediaDrm.KeyStatus> keyInformation,
+              boolean hasNewUsableKey) {
+            nativeOnKeyStatusChange(
+                mNativeMediaDrmBridge,
+                sessionId,
+                keyInformation.toArray(new MediaDrm.KeyStatus[keyInformation.size()]));
+          }
+        },
+        null);
+  }
+
+  /** Convert byte array to hex string for logging. */
+  private static String bytesToHexString(byte[] bytes) {
+    StringBuilder hexString = new StringBuilder();
+    for (int i = 0; i < bytes.length; ++i) {
+      hexString.append(HEX_CHAR_LOOKUP[bytes[i] >>> 4]);
+      hexString.append(HEX_CHAR_LOOKUP[bytes[i] & 0xf]);
+    }
+    return hexString.toString();
+  }
+
+  private void onSessionMessage(
+      int ticket, final byte[] sessionId, final MediaDrm.KeyRequest request) {
+    if (!isNativeMediaDrmBridgeValid()) {
+      return;
+    }
+
+    int requestType = MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL;
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+      requestType = request.getRequestType();
+    } else {
+      // Prior to M, getRequestType() is not supported. Do our best guess here: Assume
+      // requests with a URL are renewals and all others are initial requests.
+      requestType =
+          request.getDefaultUrl().isEmpty()
+              ? MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL
+              : MediaDrm.KeyRequest.REQUEST_TYPE_RENEWAL;
+    }
+
+    nativeOnSessionMessage(
+        mNativeMediaDrmBridge, ticket, sessionId, requestType, request.getData());
+  }
+
+  /**
+   * Get a key request.
+   *
+   * @param sessionId ID of session on which we need to get the key request.
+   * @param data Data needed to get the key request.
+   * @param mime Mime type to get the key request.
+   * @return the key request.
+   */
+  private MediaDrm.KeyRequest getKeyRequest(byte[] sessionId, byte[] data, String mime)
+      throws android.media.NotProvisionedException {
+    if (mMediaDrm == null) {
+      throw new IllegalStateException("mMediaDrm cannot be null in getKeyRequest");
+    }
+    if (mMediaCryptoSession == null) {
+      throw new IllegalStateException("mMediaCryptoSession cannot be null in getKeyRequest.");
+    }
+    // TODO: Cannot do this during provisioning pending.
+
+    HashMap<String, String> optionalParameters = new HashMap<>();
+    MediaDrm.KeyRequest request = null;
+    try {
+      request =
+          mMediaDrm.getKeyRequest(
+              sessionId, data, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
+    } catch (IllegalStateException e) {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+          && e instanceof android.media.MediaDrm.MediaDrmStateException) {
+        Log.e(TAG, "MediaDrmStateException fired during getKeyRequest().", e);
+      }
+    }
+
+    String result = (request != null) ? "succeeded" : "failed";
+    Log.d(TAG, String.format("getKeyRequest %s!", result));
+
+    return request;
+  }
+
+  /**
+   * Create a MediaCrypto object.
+   *
+   * @return false upon fatal error in creating MediaCrypto. Returns true otherwise, including the
+   *     following two cases: 1. MediaCrypto is successfully created and notified. 2. Device is not
+   *     provisioned and MediaCrypto creation will be tried again after the provisioning process is
+   *     completed.
+   *     <p>When false is returned, the caller should call release(), which will notify the native
+   *     code with a null MediaCrypto, if needed.
+   */
+  private boolean createMediaCrypto() {
+    if (mMediaDrm == null) {
+      throw new IllegalStateException("Cannot create media crypto with null mMediaDrm.");
+    }
+    if (mMediaCryptoSession != null) {
+      throw new IllegalStateException(
+          "Cannot create media crypto with non-null mMediaCryptoSession.");
+    }
+    // TODO: Cannot do this during provisioning pending.
+
+    // Open media crypto session.
+    try {
+      mMediaCryptoSession = openSession();
+    } catch (NotProvisionedException e) {
+      Log.d(TAG, "Device not provisioned", e);
+      if (!attemptProvisioning()) {
+        Log.e(TAG, "Failed to provision device during MediaCrypto creation.");
+        return false;
+      }
+      return true;
+    }
+
+    if (mMediaCryptoSession == null) {
+      Log.e(TAG, "Cannot create MediaCrypto Session.");
+      return false;
+    }
+    Log.d(
+        TAG,
+        String.format("MediaCrypto Session created: %s", bytesToHexString(mMediaCryptoSession)));
+
+    // Create MediaCrypto object.
+    try {
+      if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
+        MediaCrypto mediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession);
+        Log.d(TAG, "MediaCrypto successfully created!");
+        mMediaCrypto = mediaCrypto;
+        return true;
+      } else {
+        Log.e(TAG, "Cannot create MediaCrypto for unsupported scheme.");
+      }
+    } catch (MediaCryptoException e) {
+      Log.e(TAG, "Cannot create MediaCrypto", e);
+    }
+
+    try {
+      // Some implementations let this method throw exceptions.
+      mMediaDrm.closeSession(mMediaCryptoSession);
+    } catch (Exception e) {
+      Log.e(TAG, "closeSession failed: ", e);
+    }
+    mMediaCryptoSession = null;
+
+    return false;
+  }
+
+  /**
+   * Open a new session.
+   *
+   * @return ID of the session opened. Returns null if unexpected error happened.
+   */
+  private byte[] openSession() throws android.media.NotProvisionedException {
+    Log.d(TAG, "openSession()");
+    if (mMediaDrm == null) {
+      throw new IllegalStateException("mMediaDrm cannot be null in openSession");
+    }
+    try {
+      byte[] sessionId = mMediaDrm.openSession();
+      // Make a clone here in case the underlying byte[] is modified.
+      return sessionId.clone();
+    } catch (RuntimeException e) { // TODO: Drop this?
+      Log.e(TAG, "Cannot open a new session", e);
+      release();
+      return null;
+    } catch (NotProvisionedException e) {
+      // Throw NotProvisionedException so that we can attemptProvisioning().
+      throw e;
+    } catch (MediaDrmException e) {
+      // Other MediaDrmExceptions (e.g. ResourceBusyException) are not
+      // recoverable.
+      Log.e(TAG, "Cannot open a new session", e);
+      release();
+      return null;
+    }
+  }
+
+  /**
+   * Attempt to get the device that we are currently running on provisioned.
+   *
+   * @return whether provisioning was successful or not.
+   */
+  private boolean attemptProvisioning() {
+    Log.d(TAG, "attemptProvisioning()");
+    MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
+    String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+    byte[] response = new CobaltHttpHelper().performDrmHttpPost(url);
+    if (response == null) {
+      return false;
+    }
+    try {
+      mMediaDrm.provideProvisionResponse(response);
+      return true;
+    } catch (android.media.DeniedByServerException e) {
+      Log.e(TAG, "failed to provide provision response", e);
+    } catch (java.lang.IllegalStateException e) {
+      Log.e(TAG, "failed to provide provision response", e);
+    }
+    return false;
+  }
+
+  /**
+   * Check whether |sessionId| is an existing session ID, excluding the media crypto session.
+   *
+   * @param sessionId Crypto session Id.
+   * @return true if |sessionId| exists, false otherwise.
+   */
+  private boolean sessionExists(byte[] sessionId) {
+    if (mMediaCryptoSession == null) {
+      if (!mSessionIds.isEmpty()) {
+        throw new IllegalStateException(
+            "mSessionIds must be empty if crypto session does not exist.");
+      }
+      Log.e(TAG, "Session doesn't exist because media crypto session is not created.");
+      return false;
+    }
+    return !Arrays.equals(sessionId, mMediaCryptoSession)
+        && mSessionIds.containsKey(ByteBuffer.wrap(sessionId));
+  }
+
+  /** Release all allocated resources and finish all pending operations. */
+  private void release() {
+    // Note that mNativeMediaDrmBridge may have already been reset (see destroy()).
+    if (mMediaDrm == null) {
+      throw new IllegalStateException("Called release with null mMediaDrm.");
+    }
+
+    // Close all open sessions.
+    for (ByteBuffer sessionId : mSessionIds.keySet()) {
+      try {
+        // Some implementations don't have removeKeys.
+        // https://bugs.chromium.org/p/chromium/issues/detail?id=475632
+        mMediaDrm.removeKeys(sessionId.array());
+      } catch (Exception e) {
+        Log.e(TAG, "removeKeys failed: ", e);
+      }
+
+      try {
+        // Some implementations let this method throw exceptions.
+        mMediaDrm.closeSession(sessionId.array());
+      } catch (Exception e) {
+        Log.e(TAG, "closeSession failed: ", e);
+      }
+      Log.d(
+          TAG,
+          String.format("Successfully closed session (%s)", bytesToHexString(sessionId.array())));
+    }
+    mSessionIds.clear();
+    mSessionIds = null;
+
+    // Close mMediaCryptoSession if it's open.
+    if (mMediaCryptoSession != null) {
+      try {
+        // Some implementations let this method throw exceptions.
+        mMediaDrm.closeSession(mMediaCryptoSession);
+      } catch (Exception e) {
+        Log.e(TAG, "closeSession failed: ", e);
+      }
+      mMediaCryptoSession = null;
+    }
+
+    if (mMediaDrm != null) {
+      mMediaDrm.release();
+      mMediaDrm = null;
+    }
+  }
+
+  private boolean isNativeMediaDrmBridgeValid() {
+    return mNativeMediaDrmBridge != INVALID_NATIVE_MEDIA_DRM_BRIDGE;
+  }
+
+  private native void nativeOnSessionMessage(
+      long nativeMediaDrmBridge, int ticket, byte[] sessionId, int requestType, byte[] message);
+
+  private native void nativeOnKeyStatusChange(
+      long nativeMediaDrmBridge, byte[] sessionId, MediaDrm.KeyStatus[] keyInformation);
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java
new file mode 100644
index 0000000..899dc22
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import dev.cobalt.util.UsedByNative;
+
+/** https://wicg.github.io/mediasession/#dictdef-mediaimage */
+public class MediaImage {
+  public final String src;
+  public final String sizes;
+  public final String type;
+
+  @UsedByNative
+  public MediaImage(String src, String sizes, String type) {
+    this.src = src;
+    this.sizes = sizes;
+    this.type = type;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java
new file mode 100644
index 0000000..4287c8c
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoFrameReleaseTimeHelper.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.WindowManager;
+import dev.cobalt.util.UsedByNative;
+
+/** Makes a best effort to adjust frame release timestamps for a smoother visual result. */
+@TargetApi(16)
+@SuppressWarnings("unused")
+@UsedByNative
+public final class VideoFrameReleaseTimeHelper {
+
+  private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
+  private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+  private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+  private static final long VSYNC_OFFSET_PERCENTAGE = 80;
+  private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+  private static final long NANOS_PER_SECOND = 1000000000L;
+
+  private final VSyncSampler vsyncSampler;
+  private final boolean useDefaultDisplayVsync;
+  private final long vsyncDurationNs;
+  private final long vsyncOffsetNs;
+
+  private long lastFramePresentationTimeUs;
+  private long adjustedLastFrameTimeNs;
+  private long pendingAdjustedFrameTimeNs;
+
+  private boolean haveSync;
+  private long syncUnadjustedReleaseTimeNs;
+  private long syncFramePresentationTimeNs;
+  private long frameCount;
+
+  /**
+   * Constructs an instance that smooths frame release timestamps but does not align them with the
+   * default display's vsync signal.
+   */
+  @SuppressWarnings("unused")
+  public VideoFrameReleaseTimeHelper() {
+    this(DISPLAY_REFRESH_RATE_UNKNOWN);
+  }
+
+  /**
+   * Constructs an instance that smooths frame release timestamps and aligns them with the default
+   * display's vsync signal.
+   *
+   * @param context A context from which information about the default display can be retrieved.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public VideoFrameReleaseTimeHelper(Context context) {
+    this(getDefaultDisplayRefreshRate(context));
+  }
+
+  private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) {
+    useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
+    if (useDefaultDisplayVsync) {
+      vsyncSampler = VSyncSampler.getInstance();
+      vsyncDurationNs = (long) (NANOS_PER_SECOND / defaultDisplayRefreshRate);
+      vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+    } else {
+      vsyncSampler = null;
+      vsyncDurationNs = -1; // Value unused.
+      vsyncOffsetNs = -1; // Value unused.
+    }
+  }
+
+  /** Enables the helper. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void enable() {
+    haveSync = false;
+    if (useDefaultDisplayVsync) {
+      vsyncSampler.addObserver();
+    }
+  }
+
+  /** Disables the helper. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public void disable() {
+    if (useDefaultDisplayVsync) {
+      vsyncSampler.removeObserver();
+    }
+  }
+
+  /**
+   * Adjusts a frame release timestamp.
+   *
+   * @param framePresentationTimeUs The frame's presentation time, in microseconds.
+   * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in the
+   *     same time base as {@link System#nanoTime()}.
+   * @return The adjusted frame release timestamp, in nanoseconds and in the same time base as
+   *     {@link System#nanoTime()}.
+   */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {
+    long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+    // Until we know better, the adjustment will be a no-op.
+    long adjustedFrameTimeNs = framePresentationTimeNs;
+    long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+    if (haveSync) {
+      // See if we've advanced to the next frame.
+      if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+        frameCount++;
+        adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+      }
+      if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+        // We're synced and have waited the required number of frames to apply an adjustment.
+        // Calculate the average frame time across all the frames we've seen since the last sync.
+        // This will typically give us a frame rate at a finer granularity than the frame times
+        // themselves (which often only have millisecond granularity).
+        long averageFrameDurationNs =
+            (framePresentationTimeNs - syncFramePresentationTimeNs) / frameCount;
+        // Project the adjusted frame time forward using the average.
+        long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+        if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+          haveSync = false;
+        } else {
+          adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+          adjustedReleaseTimeNs =
+              syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs - syncFramePresentationTimeNs;
+        }
+      } else {
+        // We're synced but haven't waited the required number of frames to apply an adjustment.
+        // Check drift anyway.
+        if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+          haveSync = false;
+        }
+      }
+    }
+
+    // If we need to sync, do so now.
+    if (!haveSync) {
+      syncFramePresentationTimeNs = framePresentationTimeNs;
+      syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+      frameCount = 0;
+      haveSync = true;
+      onSynced();
+    }
+
+    lastFramePresentationTimeUs = framePresentationTimeUs;
+    pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+    if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+      return adjustedReleaseTimeNs;
+    }
+
+    // Find the timestamp of the closest vsync. This is the vsync that we're targeting.
+    long snappedTimeNs =
+        closestVsync(adjustedReleaseTimeNs, vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+    // Apply an offset so that we release before the target vsync, but after the previous one.
+    return snappedTimeNs - vsyncOffsetNs;
+  }
+
+  protected void onSynced() {
+    // Do nothing.
+  }
+
+  private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
+    long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+    long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+    return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
+  }
+
+  private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
+    long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+    long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+    long snappedBeforeNs;
+    long snappedAfterNs;
+    if (releaseTime <= snappedTimeNs) {
+      snappedBeforeNs = snappedTimeNs - vsyncDuration;
+      snappedAfterNs = snappedTimeNs;
+    } else {
+      snappedBeforeNs = snappedTimeNs;
+      snappedAfterNs = snappedTimeNs + vsyncDuration;
+    }
+    long snappedAfterDiff = snappedAfterNs - releaseTime;
+    long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+    return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
+  }
+
+  private static double getDefaultDisplayRefreshRate(Context context) {
+    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    return manager.getDefaultDisplay() != null
+        ? manager.getDefaultDisplay().getRefreshRate()
+        : DISPLAY_REFRESH_RATE_UNKNOWN;
+  }
+
+  /**
+   * Samples display vsync timestamps. A single instance using a single {@link Choreographer} is
+   * shared by all {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a resource
+   * leak in the platform on API levels prior to 23.
+   */
+  private static final class VSyncSampler implements FrameCallback, Handler.Callback {
+
+    public volatile long sampledVsyncTimeNs;
+
+    private static final int CREATE_CHOREOGRAPHER = 0;
+    private static final int MSG_ADD_OBSERVER = 1;
+    private static final int MSG_REMOVE_OBSERVER = 2;
+
+    private static final VSyncSampler INSTANCE = new VSyncSampler();
+
+    private final Handler handler;
+    private final HandlerThread choreographerOwnerThread;
+    private Choreographer choreographer;
+    private int observerCount;
+
+    public static VSyncSampler getInstance() {
+      return INSTANCE;
+    }
+
+    private VSyncSampler() {
+      choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
+      choreographerOwnerThread.start();
+      handler = new Handler(choreographerOwnerThread.getLooper(), this);
+      handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+    }
+
+    /**
+     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is observing {@link
+     * #sampledVsyncTimeNs}, and hence that the value should be periodically updated.
+     */
+    public void addObserver() {
+      handler.sendEmptyMessage(MSG_ADD_OBSERVER);
+    }
+
+    /**
+     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no longer observing {@link
+     * #sampledVsyncTimeNs}.
+     */
+    public void removeObserver() {
+      handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);
+    }
+
+    @Override
+    public void doFrame(long vsyncTimeNs) {
+      sampledVsyncTimeNs = vsyncTimeNs;
+      choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+    }
+
+    @Override
+    public boolean handleMessage(Message message) {
+      switch (message.what) {
+        case CREATE_CHOREOGRAPHER:
+          {
+            createChoreographerInstanceInternal();
+            return true;
+          }
+        case MSG_ADD_OBSERVER:
+          {
+            addObserverInternal();
+            return true;
+          }
+        case MSG_REMOVE_OBSERVER:
+          {
+            removeObserverInternal();
+            return true;
+          }
+        default:
+          {
+            return false;
+          }
+      }
+    }
+
+    private void createChoreographerInstanceInternal() {
+      choreographer = Choreographer.getInstance();
+    }
+
+    private void addObserverInternal() {
+      observerCount++;
+      if (observerCount == 1) {
+        choreographer.postFrameCallback(this);
+      }
+    }
+
+    private void removeObserverInternal() {
+      observerCount--;
+      if (observerCount == 0) {
+        choreographer.removeFrameCallback(this);
+        sampledVsyncTimeNs = 0;
+      }
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java
new file mode 100644
index 0000000..2478b97
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java
@@ -0,0 +1,108 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.media;
+
+import static dev.cobalt.media.Log.TAG;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import dev.cobalt.util.Log;
+
+/**
+ * A Surface view to be used by the video decoder. It informs the Starboard application when the
+ * surface is available so that the decoder can get a reference to it.
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+  public static native void nativeOnLayoutNeeded();
+
+  public static native void nativeOnLayoutScheduled();
+
+  public static native void nativeOnGlobalLayout();
+
+  private Rect videoBounds;
+
+  public VideoSurfaceView(Context context) {
+    super(context);
+    initialize(context);
+  }
+
+  public VideoSurfaceView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    initialize(context);
+  }
+
+  public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+    super(context, attrs, defStyleAttr);
+    initialize(context);
+  }
+
+  public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+    super(context, attrs, defStyleAttr, defStyleRes);
+    initialize(context);
+  }
+
+  private void initialize(Context context) {
+    videoBounds = new Rect();
+    setBackgroundColor(Color.TRANSPARENT);
+    getHolder().addCallback(new SurfaceHolderCallback());
+
+    // TODO: Avoid recreating the surface when the player bounds change.
+    // Recreating the surface is time-consuming and complicates synchronizing
+    // punch-out video when the position / size is animated.
+  }
+
+  public boolean updateVideoBounds(final int x, final int y, final int width, final int height) {
+    if (videoBounds.left != x
+        || videoBounds.top != y
+        || videoBounds.right != x + width
+        || videoBounds.bottom != y + height) {
+      videoBounds.set(x, y, x + width, y + height);
+      return true;
+    }
+    return false;
+  }
+
+  private native void nativeOnVideoSurfaceChanged(Surface surface);
+
+  private class SurfaceHolderCallback implements SurfaceHolder.Callback {
+
+    boolean sawInitialChange = false;
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+      nativeOnVideoSurfaceChanged(holder.getSurface());
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+      // We should only ever see the initial change after creation.
+      if (sawInitialChange) {
+        Log.e(TAG, "Video surface changed; decoding may break");
+      }
+      sawInitialChange = true;
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+      nativeOnVideoSurfaceChanged(null);
+    }
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
new file mode 100644
index 0000000..315e679
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
@@ -0,0 +1,124 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.storage;
+
+import static dev.cobalt.util.Log.TAG;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.google.protobuf.InvalidProtocolBufferException;
+import dev.cobalt.storage.StorageProto.Storage;
+import dev.cobalt.util.Log;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/** A class to load Cobalt storage from the file system. */
+public class CobaltStorageLoader {
+  private final File filesDir;
+
+  private static final String STORAGE_NAME_PREFIX = ".starboard";
+  private static final String STORAGE_NAME_SUFFIX = ".storage";
+  private static final String STORAGE_HEADER = "SAV1";
+
+  /**
+   * Initializes the loader with the files directory root.
+   *
+   * @param filesDir a File object representing the files directory.
+   */
+  public CobaltStorageLoader(File filesDir) {
+    if (filesDir == null) {
+      throw new IllegalArgumentException("A valid filesDir object is required");
+    }
+    this.filesDir = filesDir;
+  }
+
+  /**
+   * Reads synchronously the Cobalt storage from the file system.
+   *
+   * @return a snapshot of the Cobalt storage as a proto message.
+   */
+  public Storage loadStorageSnapshot() {
+    String fileName = getStorageFileName();
+    if (fileName == null) {
+      return Storage.getDefaultInstance();
+    }
+    byte[] storageBlob = getStorageBlob(fileName);
+    if (storageBlob == null) {
+      Log.e(TAG, "Failed to get storage blob");
+      return Storage.getDefaultInstance();
+    }
+    if (!validateStorageBlob(storageBlob)) {
+      Log.e(TAG, "Invalid storage blob");
+      return Storage.getDefaultInstance();
+    }
+    storageBlob = Arrays.copyOfRange(storageBlob, STORAGE_HEADER.length(), storageBlob.length);
+    try {
+      return Storage.parseFrom(storageBlob);
+    } catch (InvalidProtocolBufferException e) {
+      Log.e(TAG, "Failed parsing the blob", e);
+    }
+    return Storage.getDefaultInstance();
+  }
+
+  private boolean validateStorageBlob(byte[] storageBlob) {
+    if (storageBlob == null || storageBlob.length < STORAGE_HEADER.length()) {
+      return false;
+    }
+    String header = new String(storageBlob, 0, STORAGE_HEADER.length());
+    return header.equals(STORAGE_HEADER);
+  }
+
+  @Nullable
+  private byte[] getStorageBlob(String fileName) {
+    if (TextUtils.isEmpty(fileName)) {
+      Log.e(TAG, "Invalid empty file name");
+      return null;
+    }
+    try (FileInputStream in = new FileInputStream(new File(filesDir, fileName));
+        ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+      byte[] buffer = new byte[8192];
+      int len;
+      while ((len = in.read(buffer)) != -1) {
+        out.write(buffer, 0, len);
+      }
+      return out.toByteArray();
+    } catch (IOException e) {
+      Log.e(TAG, "Failed to read storage blob", e);
+    }
+    return null;
+  }
+
+  @Nullable
+  private String getStorageFileName() {
+    String[] fileNames = filesDir.list();
+    if (fileNames == null) {
+      Log.w(TAG, "Empty file list");
+      return null;
+    }
+    for (String fileName : fileNames) {
+      if (fileName.startsWith(STORAGE_NAME_PREFIX) && fileName.endsWith(STORAGE_NAME_SUFFIX)) {
+        File storageFile = new File(filesDir, fileName);
+        if (storageFile.length() > 0) {
+          return fileName;
+        }
+      }
+    }
+    Log.w(TAG, "Failed to find storage file name");
+    return null;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md
new file mode 100644
index 0000000..7938fc5
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/README.md
@@ -0,0 +1,15 @@
+The StorageProto java class was generated using the protoc
+compiler from the third_party/protobuf package.
+
+Example:
+ out/linux-x64x11_debug/protoc cobalt/storage/store/storage.proto \
+     --java_out=starboard/android/apk/app/src/main/java
+
+The code doesn't compile cleanly and produces warning:
+ warning: [unchecked] unchecked conversion
+
+To suppress the warning the following annotation should be added
+to the StorageProto class:
+
+@SuppressWarnings("unchecked")
+
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java
new file mode 100644
index 0000000..a07b35d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/StorageProto.java
@@ -0,0 +1,3591 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: cobalt/storage/store/storage.proto
+
+package dev.cobalt.storage;
+
+@SuppressWarnings("unchecked")
+public final class StorageProto {
+  private StorageProto() {}
+  public static void registerAllExtensions(
+      com.google.protobuf.ExtensionRegistryLite registry) {
+  }
+  public interface CookieOrBuilder extends
+      // @@protoc_insertion_point(interface_extends:cobalt.storage.Cookie)
+      com.google.protobuf.MessageLiteOrBuilder {
+
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    java.lang.String getName();
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    com.google.protobuf.ByteString
+        getNameBytes();
+
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    java.lang.String getValue();
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    com.google.protobuf.ByteString
+        getValueBytes();
+
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    java.lang.String getDomain();
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    com.google.protobuf.ByteString
+        getDomainBytes();
+
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    java.lang.String getPath();
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    com.google.protobuf.ByteString
+        getPathBytes();
+
+    /**
+     * <pre>
+     * The creation time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 creation_time_us = 5;</code>
+     */
+    long getCreationTimeUs();
+
+    /**
+     * <pre>
+     * The expiration time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 expiration_time_us = 6;</code>
+     */
+    long getExpirationTimeUs();
+
+    /**
+     * <pre>
+     * The last access time in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 last_access_time_us = 7;</code>
+     */
+    long getLastAccessTimeUs();
+
+    /**
+     * <pre>
+     * Whether the cookie should be transmitted only over secure connection.
+     * Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool secure = 8;</code>
+     */
+    boolean getSecure();
+
+    /**
+     * <pre>
+     * Whether this is an HTTP-only cookie. Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool http_only = 9;</code>
+     */
+    boolean getHttpOnly();
+  }
+  /**
+   * <pre>
+   * A single cookie representation.
+   * </pre>
+   *
+   * Protobuf type {@code cobalt.storage.Cookie}
+   */
+  public  static final class Cookie extends
+      com.google.protobuf.GeneratedMessageLite<
+          Cookie, Cookie.Builder> implements
+      // @@protoc_insertion_point(message_implements:cobalt.storage.Cookie)
+      CookieOrBuilder {
+    private Cookie() {
+      name_ = "";
+      value_ = "";
+      domain_ = "";
+      path_ = "";
+      creationTimeUs_ = 0L;
+      expirationTimeUs_ = 0L;
+      lastAccessTimeUs_ = 0L;
+      secure_ = false;
+      httpOnly_ = false;
+    }
+    public static final int NAME_FIELD_NUMBER = 1;
+    private java.lang.String name_;
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    public java.lang.String getName() {
+      return name_;
+    }
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    public com.google.protobuf.ByteString
+        getNameBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(name_);
+    }
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    private void setName(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      name_ = value;
+    }
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    private void clearName() {
+      
+      name_ = getDefaultInstance().getName();
+    }
+    /**
+     * <pre>
+     * The name of the cookie.
+     * </pre>
+     *
+     * <code>optional string name = 1;</code>
+     */
+    private void setNameBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      name_ = value.toStringUtf8();
+    }
+
+    public static final int VALUE_FIELD_NUMBER = 2;
+    private java.lang.String value_;
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    public java.lang.String getValue() {
+      return value_;
+    }
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    public com.google.protobuf.ByteString
+        getValueBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(value_);
+    }
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void setValue(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      value_ = value;
+    }
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void clearValue() {
+      
+      value_ = getDefaultInstance().getValue();
+    }
+    /**
+     * <pre>
+     * The value of the cookie.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void setValueBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      value_ = value.toStringUtf8();
+    }
+
+    public static final int DOMAIN_FIELD_NUMBER = 3;
+    private java.lang.String domain_;
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    public java.lang.String getDomain() {
+      return domain_;
+    }
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    public com.google.protobuf.ByteString
+        getDomainBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(domain_);
+    }
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    private void setDomain(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      domain_ = value;
+    }
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    private void clearDomain() {
+      
+      domain_ = getDefaultInstance().getDomain();
+    }
+    /**
+     * <pre>
+     * The domain of the url for which the cookie is store.
+     * </pre>
+     *
+     * <code>optional string domain = 3;</code>
+     */
+    private void setDomainBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      domain_ = value.toStringUtf8();
+    }
+
+    public static final int PATH_FIELD_NUMBER = 4;
+    private java.lang.String path_;
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    public java.lang.String getPath() {
+      return path_;
+    }
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    public com.google.protobuf.ByteString
+        getPathBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(path_);
+    }
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    private void setPath(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      path_ = value;
+    }
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    private void clearPath() {
+      
+      path_ = getDefaultInstance().getPath();
+    }
+    /**
+     * <pre>
+     * The path of the url for which the cookie is stored.
+     * </pre>
+     *
+     * <code>optional string path = 4;</code>
+     */
+    private void setPathBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      path_ = value.toStringUtf8();
+    }
+
+    public static final int CREATION_TIME_US_FIELD_NUMBER = 5;
+    private long creationTimeUs_;
+    /**
+     * <pre>
+     * The creation time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 creation_time_us = 5;</code>
+     */
+    public long getCreationTimeUs() {
+      return creationTimeUs_;
+    }
+    /**
+     * <pre>
+     * The creation time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 creation_time_us = 5;</code>
+     */
+    private void setCreationTimeUs(long value) {
+      
+      creationTimeUs_ = value;
+    }
+    /**
+     * <pre>
+     * The creation time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 creation_time_us = 5;</code>
+     */
+    private void clearCreationTimeUs() {
+      
+      creationTimeUs_ = 0L;
+    }
+
+    public static final int EXPIRATION_TIME_US_FIELD_NUMBER = 6;
+    private long expirationTimeUs_;
+    /**
+     * <pre>
+     * The expiration time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 expiration_time_us = 6;</code>
+     */
+    public long getExpirationTimeUs() {
+      return expirationTimeUs_;
+    }
+    /**
+     * <pre>
+     * The expiration time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 expiration_time_us = 6;</code>
+     */
+    private void setExpirationTimeUs(long value) {
+      
+      expirationTimeUs_ = value;
+    }
+    /**
+     * <pre>
+     * The expiration time for the cookie in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 expiration_time_us = 6;</code>
+     */
+    private void clearExpirationTimeUs() {
+      
+      expirationTimeUs_ = 0L;
+    }
+
+    public static final int LAST_ACCESS_TIME_US_FIELD_NUMBER = 7;
+    private long lastAccessTimeUs_;
+    /**
+     * <pre>
+     * The last access time in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 last_access_time_us = 7;</code>
+     */
+    public long getLastAccessTimeUs() {
+      return lastAccessTimeUs_;
+    }
+    /**
+     * <pre>
+     * The last access time in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 last_access_time_us = 7;</code>
+     */
+    private void setLastAccessTimeUs(long value) {
+      
+      lastAccessTimeUs_ = value;
+    }
+    /**
+     * <pre>
+     * The last access time in microseconds since
+     * Windows epoch - 1/1/1601 UTC.
+     * </pre>
+     *
+     * <code>optional int64 last_access_time_us = 7;</code>
+     */
+    private void clearLastAccessTimeUs() {
+      
+      lastAccessTimeUs_ = 0L;
+    }
+
+    public static final int SECURE_FIELD_NUMBER = 8;
+    private boolean secure_;
+    /**
+     * <pre>
+     * Whether the cookie should be transmitted only over secure connection.
+     * Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool secure = 8;</code>
+     */
+    public boolean getSecure() {
+      return secure_;
+    }
+    /**
+     * <pre>
+     * Whether the cookie should be transmitted only over secure connection.
+     * Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool secure = 8;</code>
+     */
+    private void setSecure(boolean value) {
+      
+      secure_ = value;
+    }
+    /**
+     * <pre>
+     * Whether the cookie should be transmitted only over secure connection.
+     * Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool secure = 8;</code>
+     */
+    private void clearSecure() {
+      
+      secure_ = false;
+    }
+
+    public static final int HTTP_ONLY_FIELD_NUMBER = 9;
+    private boolean httpOnly_;
+    /**
+     * <pre>
+     * Whether this is an HTTP-only cookie. Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool http_only = 9;</code>
+     */
+    public boolean getHttpOnly() {
+      return httpOnly_;
+    }
+    /**
+     * <pre>
+     * Whether this is an HTTP-only cookie. Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool http_only = 9;</code>
+     */
+    private void setHttpOnly(boolean value) {
+      
+      httpOnly_ = value;
+    }
+    /**
+     * <pre>
+     * Whether this is an HTTP-only cookie. Defaults to false.
+     * </pre>
+     *
+     * <code>optional bool http_only = 9;</code>
+     */
+    private void clearHttpOnly() {
+      
+      httpOnly_ = false;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      if (!name_.isEmpty()) {
+        output.writeString(1, getName());
+      }
+      if (!value_.isEmpty()) {
+        output.writeString(2, getValue());
+      }
+      if (!domain_.isEmpty()) {
+        output.writeString(3, getDomain());
+      }
+      if (!path_.isEmpty()) {
+        output.writeString(4, getPath());
+      }
+      if (creationTimeUs_ != 0L) {
+        output.writeInt64(5, creationTimeUs_);
+      }
+      if (expirationTimeUs_ != 0L) {
+        output.writeInt64(6, expirationTimeUs_);
+      }
+      if (lastAccessTimeUs_ != 0L) {
+        output.writeInt64(7, lastAccessTimeUs_);
+      }
+      if (secure_ != false) {
+        output.writeBool(8, secure_);
+      }
+      if (httpOnly_ != false) {
+        output.writeBool(9, httpOnly_);
+      }
+    }
+
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (!name_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(1, getName());
+      }
+      if (!value_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(2, getValue());
+      }
+      if (!domain_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(3, getDomain());
+      }
+      if (!path_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(4, getPath());
+      }
+      if (creationTimeUs_ != 0L) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(5, creationTimeUs_);
+      }
+      if (expirationTimeUs_ != 0L) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(6, expirationTimeUs_);
+      }
+      if (lastAccessTimeUs_ != 0L) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(7, lastAccessTimeUs_);
+      }
+      if (secure_ != false) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBoolSize(8, secure_);
+      }
+      if (httpOnly_ != false) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBoolSize(9, httpOnly_);
+      }
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Cookie parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() {
+      return DEFAULT_INSTANCE.toBuilder();
+    }
+    public static Builder newBuilder(dev.cobalt.storage.StorageProto.Cookie prototype) {
+      return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+    }
+
+    /**
+     * <pre>
+     * A single cookie representation.
+     * </pre>
+     *
+     * Protobuf type {@code cobalt.storage.Cookie}
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessageLite.Builder<
+          dev.cobalt.storage.StorageProto.Cookie, Builder> implements
+        // @@protoc_insertion_point(builder_implements:cobalt.storage.Cookie)
+        dev.cobalt.storage.StorageProto.CookieOrBuilder {
+      // Construct using dev.cobalt.storage.StorageProto.Cookie.newBuilder()
+      private Builder() {
+        super(DEFAULT_INSTANCE);
+      }
+
+
+      /**
+       * <pre>
+       * The name of the cookie.
+       * </pre>
+       *
+       * <code>optional string name = 1;</code>
+       */
+      public java.lang.String getName() {
+        return instance.getName();
+      }
+      /**
+       * <pre>
+       * The name of the cookie.
+       * </pre>
+       *
+       * <code>optional string name = 1;</code>
+       */
+      public com.google.protobuf.ByteString
+          getNameBytes() {
+        return instance.getNameBytes();
+      }
+      /**
+       * <pre>
+       * The name of the cookie.
+       * </pre>
+       *
+       * <code>optional string name = 1;</code>
+       */
+      public Builder setName(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setName(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The name of the cookie.
+       * </pre>
+       *
+       * <code>optional string name = 1;</code>
+       */
+      public Builder clearName() {
+        copyOnWrite();
+        instance.clearName();
+        return this;
+      }
+      /**
+       * <pre>
+       * The name of the cookie.
+       * </pre>
+       *
+       * <code>optional string name = 1;</code>
+       */
+      public Builder setNameBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setNameBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The value of the cookie.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public java.lang.String getValue() {
+        return instance.getValue();
+      }
+      /**
+       * <pre>
+       * The value of the cookie.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public com.google.protobuf.ByteString
+          getValueBytes() {
+        return instance.getValueBytes();
+      }
+      /**
+       * <pre>
+       * The value of the cookie.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder setValue(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setValue(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The value of the cookie.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder clearValue() {
+        copyOnWrite();
+        instance.clearValue();
+        return this;
+      }
+      /**
+       * <pre>
+       * The value of the cookie.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder setValueBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setValueBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The domain of the url for which the cookie is store.
+       * </pre>
+       *
+       * <code>optional string domain = 3;</code>
+       */
+      public java.lang.String getDomain() {
+        return instance.getDomain();
+      }
+      /**
+       * <pre>
+       * The domain of the url for which the cookie is store.
+       * </pre>
+       *
+       * <code>optional string domain = 3;</code>
+       */
+      public com.google.protobuf.ByteString
+          getDomainBytes() {
+        return instance.getDomainBytes();
+      }
+      /**
+       * <pre>
+       * The domain of the url for which the cookie is store.
+       * </pre>
+       *
+       * <code>optional string domain = 3;</code>
+       */
+      public Builder setDomain(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setDomain(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The domain of the url for which the cookie is store.
+       * </pre>
+       *
+       * <code>optional string domain = 3;</code>
+       */
+      public Builder clearDomain() {
+        copyOnWrite();
+        instance.clearDomain();
+        return this;
+      }
+      /**
+       * <pre>
+       * The domain of the url for which the cookie is store.
+       * </pre>
+       *
+       * <code>optional string domain = 3;</code>
+       */
+      public Builder setDomainBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setDomainBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The path of the url for which the cookie is stored.
+       * </pre>
+       *
+       * <code>optional string path = 4;</code>
+       */
+      public java.lang.String getPath() {
+        return instance.getPath();
+      }
+      /**
+       * <pre>
+       * The path of the url for which the cookie is stored.
+       * </pre>
+       *
+       * <code>optional string path = 4;</code>
+       */
+      public com.google.protobuf.ByteString
+          getPathBytes() {
+        return instance.getPathBytes();
+      }
+      /**
+       * <pre>
+       * The path of the url for which the cookie is stored.
+       * </pre>
+       *
+       * <code>optional string path = 4;</code>
+       */
+      public Builder setPath(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setPath(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The path of the url for which the cookie is stored.
+       * </pre>
+       *
+       * <code>optional string path = 4;</code>
+       */
+      public Builder clearPath() {
+        copyOnWrite();
+        instance.clearPath();
+        return this;
+      }
+      /**
+       * <pre>
+       * The path of the url for which the cookie is stored.
+       * </pre>
+       *
+       * <code>optional string path = 4;</code>
+       */
+      public Builder setPathBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setPathBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The creation time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 creation_time_us = 5;</code>
+       */
+      public long getCreationTimeUs() {
+        return instance.getCreationTimeUs();
+      }
+      /**
+       * <pre>
+       * The creation time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 creation_time_us = 5;</code>
+       */
+      public Builder setCreationTimeUs(long value) {
+        copyOnWrite();
+        instance.setCreationTimeUs(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The creation time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 creation_time_us = 5;</code>
+       */
+      public Builder clearCreationTimeUs() {
+        copyOnWrite();
+        instance.clearCreationTimeUs();
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The expiration time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 expiration_time_us = 6;</code>
+       */
+      public long getExpirationTimeUs() {
+        return instance.getExpirationTimeUs();
+      }
+      /**
+       * <pre>
+       * The expiration time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 expiration_time_us = 6;</code>
+       */
+      public Builder setExpirationTimeUs(long value) {
+        copyOnWrite();
+        instance.setExpirationTimeUs(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The expiration time for the cookie in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 expiration_time_us = 6;</code>
+       */
+      public Builder clearExpirationTimeUs() {
+        copyOnWrite();
+        instance.clearExpirationTimeUs();
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The last access time in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 last_access_time_us = 7;</code>
+       */
+      public long getLastAccessTimeUs() {
+        return instance.getLastAccessTimeUs();
+      }
+      /**
+       * <pre>
+       * The last access time in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 last_access_time_us = 7;</code>
+       */
+      public Builder setLastAccessTimeUs(long value) {
+        copyOnWrite();
+        instance.setLastAccessTimeUs(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The last access time in microseconds since
+       * Windows epoch - 1/1/1601 UTC.
+       * </pre>
+       *
+       * <code>optional int64 last_access_time_us = 7;</code>
+       */
+      public Builder clearLastAccessTimeUs() {
+        copyOnWrite();
+        instance.clearLastAccessTimeUs();
+        return this;
+      }
+
+      /**
+       * <pre>
+       * Whether the cookie should be transmitted only over secure connection.
+       * Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool secure = 8;</code>
+       */
+      public boolean getSecure() {
+        return instance.getSecure();
+      }
+      /**
+       * <pre>
+       * Whether the cookie should be transmitted only over secure connection.
+       * Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool secure = 8;</code>
+       */
+      public Builder setSecure(boolean value) {
+        copyOnWrite();
+        instance.setSecure(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * Whether the cookie should be transmitted only over secure connection.
+       * Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool secure = 8;</code>
+       */
+      public Builder clearSecure() {
+        copyOnWrite();
+        instance.clearSecure();
+        return this;
+      }
+
+      /**
+       * <pre>
+       * Whether this is an HTTP-only cookie. Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool http_only = 9;</code>
+       */
+      public boolean getHttpOnly() {
+        return instance.getHttpOnly();
+      }
+      /**
+       * <pre>
+       * Whether this is an HTTP-only cookie. Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool http_only = 9;</code>
+       */
+      public Builder setHttpOnly(boolean value) {
+        copyOnWrite();
+        instance.setHttpOnly(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * Whether this is an HTTP-only cookie. Defaults to false.
+       * </pre>
+       *
+       * <code>optional bool http_only = 9;</code>
+       */
+      public Builder clearHttpOnly() {
+        copyOnWrite();
+        instance.clearHttpOnly();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:cobalt.storage.Cookie)
+    }
+    protected final Object dynamicMethod(
+        com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+        Object arg0, Object arg1) {
+      switch (method) {
+        case NEW_MUTABLE_INSTANCE: {
+          return new dev.cobalt.storage.StorageProto.Cookie();
+        }
+        case IS_INITIALIZED: {
+          return DEFAULT_INSTANCE;
+        }
+        case MAKE_IMMUTABLE: {
+          return null;
+        }
+        case NEW_BUILDER: {
+          return new Builder();
+        }
+        case VISIT: {
+          Visitor visitor = (Visitor) arg0;
+          dev.cobalt.storage.StorageProto.Cookie other = (dev.cobalt.storage.StorageProto.Cookie) arg1;
+          name_ = visitor.visitString(!name_.isEmpty(), name_,
+              !other.name_.isEmpty(), other.name_);
+          value_ = visitor.visitString(!value_.isEmpty(), value_,
+              !other.value_.isEmpty(), other.value_);
+          domain_ = visitor.visitString(!domain_.isEmpty(), domain_,
+              !other.domain_.isEmpty(), other.domain_);
+          path_ = visitor.visitString(!path_.isEmpty(), path_,
+              !other.path_.isEmpty(), other.path_);
+          creationTimeUs_ = visitor.visitLong(creationTimeUs_ != 0L, creationTimeUs_,
+              other.creationTimeUs_ != 0L, other.creationTimeUs_);
+          expirationTimeUs_ = visitor.visitLong(expirationTimeUs_ != 0L, expirationTimeUs_,
+              other.expirationTimeUs_ != 0L, other.expirationTimeUs_);
+          lastAccessTimeUs_ = visitor.visitLong(lastAccessTimeUs_ != 0L, lastAccessTimeUs_,
+              other.lastAccessTimeUs_ != 0L, other.lastAccessTimeUs_);
+          secure_ = visitor.visitBoolean(secure_ != false, secure_,
+              other.secure_ != false, other.secure_);
+          httpOnly_ = visitor.visitBoolean(httpOnly_ != false, httpOnly_,
+              other.httpOnly_ != false, other.httpOnly_);
+          if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+              .INSTANCE) {
+          }
+          return this;
+        }
+        case MERGE_FROM_STREAM: {
+          com.google.protobuf.CodedInputStream input =
+              (com.google.protobuf.CodedInputStream) arg0;
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+              (com.google.protobuf.ExtensionRegistryLite) arg1;
+          try {
+            boolean done = false;
+            while (!done) {
+              int tag = input.readTag();
+              switch (tag) {
+                case 0:
+                  done = true;
+                  break;
+                default: {
+                  if (!input.skipField(tag)) {
+                    done = true;
+                  }
+                  break;
+                }
+                case 10: {
+                  String s = input.readStringRequireUtf8();
+
+                  name_ = s;
+                  break;
+                }
+                case 18: {
+                  String s = input.readStringRequireUtf8();
+
+                  value_ = s;
+                  break;
+                }
+                case 26: {
+                  String s = input.readStringRequireUtf8();
+
+                  domain_ = s;
+                  break;
+                }
+                case 34: {
+                  String s = input.readStringRequireUtf8();
+
+                  path_ = s;
+                  break;
+                }
+                case 40: {
+
+                  creationTimeUs_ = input.readInt64();
+                  break;
+                }
+                case 48: {
+
+                  expirationTimeUs_ = input.readInt64();
+                  break;
+                }
+                case 56: {
+
+                  lastAccessTimeUs_ = input.readInt64();
+                  break;
+                }
+                case 64: {
+
+                  secure_ = input.readBool();
+                  break;
+                }
+                case 72: {
+
+                  httpOnly_ = input.readBool();
+                  break;
+                }
+              }
+            }
+          } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            throw new RuntimeException(e.setUnfinishedMessage(this));
+          } catch (java.io.IOException e) {
+            throw new RuntimeException(
+                new com.google.protobuf.InvalidProtocolBufferException(
+                    e.getMessage()).setUnfinishedMessage(this));
+          } finally {
+          }
+        }
+        case GET_DEFAULT_INSTANCE: {
+          return DEFAULT_INSTANCE;
+        }
+        case GET_PARSER: {
+          if (PARSER == null) {    synchronized (dev.cobalt.storage.StorageProto.Cookie.class) {
+              if (PARSER == null) {
+                PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+              }
+            }
+          }
+          return PARSER;
+        }
+      }
+      throw new UnsupportedOperationException();
+    }
+
+
+    // @@protoc_insertion_point(class_scope:cobalt.storage.Cookie)
+    private static final dev.cobalt.storage.StorageProto.Cookie DEFAULT_INSTANCE;
+    static {
+      DEFAULT_INSTANCE = new Cookie();
+      DEFAULT_INSTANCE.makeImmutable();
+    }
+
+    public static dev.cobalt.storage.StorageProto.Cookie getDefaultInstance() {
+      return DEFAULT_INSTANCE;
+    }
+
+    private static volatile com.google.protobuf.Parser<Cookie> PARSER;
+
+    public static com.google.protobuf.Parser<Cookie> parser() {
+      return DEFAULT_INSTANCE.getParserForType();
+    }
+  }
+
+  public interface LocalStorageEntryOrBuilder extends
+      // @@protoc_insertion_point(interface_extends:cobalt.storage.LocalStorageEntry)
+      com.google.protobuf.MessageLiteOrBuilder {
+
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    java.lang.String getKey();
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    com.google.protobuf.ByteString
+        getKeyBytes();
+
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    java.lang.String getValue();
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    com.google.protobuf.ByteString
+        getValueBytes();
+  }
+  /**
+   * <pre>
+   * A single local storage entry.
+   * </pre>
+   *
+   * Protobuf type {@code cobalt.storage.LocalStorageEntry}
+   */
+  public  static final class LocalStorageEntry extends
+      com.google.protobuf.GeneratedMessageLite<
+          LocalStorageEntry, LocalStorageEntry.Builder> implements
+      // @@protoc_insertion_point(message_implements:cobalt.storage.LocalStorageEntry)
+      LocalStorageEntryOrBuilder {
+    private LocalStorageEntry() {
+      key_ = "";
+      value_ = "";
+    }
+    public static final int KEY_FIELD_NUMBER = 1;
+    private java.lang.String key_;
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    public java.lang.String getKey() {
+      return key_;
+    }
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    public com.google.protobuf.ByteString
+        getKeyBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(key_);
+    }
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    private void setKey(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      key_ = value;
+    }
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    private void clearKey() {
+      
+      key_ = getDefaultInstance().getKey();
+    }
+    /**
+     * <pre>
+     * The key for the local storage entry.
+     * </pre>
+     *
+     * <code>optional string key = 1;</code>
+     */
+    private void setKeyBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      key_ = value.toStringUtf8();
+    }
+
+    public static final int VALUE_FIELD_NUMBER = 2;
+    private java.lang.String value_;
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    public java.lang.String getValue() {
+      return value_;
+    }
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    public com.google.protobuf.ByteString
+        getValueBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(value_);
+    }
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void setValue(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      value_ = value;
+    }
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void clearValue() {
+      
+      value_ = getDefaultInstance().getValue();
+    }
+    /**
+     * <pre>
+     * The value of the local storage entry.
+     * </pre>
+     *
+     * <code>optional string value = 2;</code>
+     */
+    private void setValueBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      value_ = value.toStringUtf8();
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      if (!key_.isEmpty()) {
+        output.writeString(1, getKey());
+      }
+      if (!value_.isEmpty()) {
+        output.writeString(2, getValue());
+      }
+    }
+
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (!key_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(1, getKey());
+      }
+      if (!value_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(2, getValue());
+      }
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() {
+      return DEFAULT_INSTANCE.toBuilder();
+    }
+    public static Builder newBuilder(dev.cobalt.storage.StorageProto.LocalStorageEntry prototype) {
+      return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+    }
+
+    /**
+     * <pre>
+     * A single local storage entry.
+     * </pre>
+     *
+     * Protobuf type {@code cobalt.storage.LocalStorageEntry}
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessageLite.Builder<
+          dev.cobalt.storage.StorageProto.LocalStorageEntry, Builder> implements
+        // @@protoc_insertion_point(builder_implements:cobalt.storage.LocalStorageEntry)
+        dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder {
+      // Construct using dev.cobalt.storage.StorageProto.LocalStorageEntry.newBuilder()
+      private Builder() {
+        super(DEFAULT_INSTANCE);
+      }
+
+
+      /**
+       * <pre>
+       * The key for the local storage entry.
+       * </pre>
+       *
+       * <code>optional string key = 1;</code>
+       */
+      public java.lang.String getKey() {
+        return instance.getKey();
+      }
+      /**
+       * <pre>
+       * The key for the local storage entry.
+       * </pre>
+       *
+       * <code>optional string key = 1;</code>
+       */
+      public com.google.protobuf.ByteString
+          getKeyBytes() {
+        return instance.getKeyBytes();
+      }
+      /**
+       * <pre>
+       * The key for the local storage entry.
+       * </pre>
+       *
+       * <code>optional string key = 1;</code>
+       */
+      public Builder setKey(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setKey(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The key for the local storage entry.
+       * </pre>
+       *
+       * <code>optional string key = 1;</code>
+       */
+      public Builder clearKey() {
+        copyOnWrite();
+        instance.clearKey();
+        return this;
+      }
+      /**
+       * <pre>
+       * The key for the local storage entry.
+       * </pre>
+       *
+       * <code>optional string key = 1;</code>
+       */
+      public Builder setKeyBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setKeyBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The value of the local storage entry.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public java.lang.String getValue() {
+        return instance.getValue();
+      }
+      /**
+       * <pre>
+       * The value of the local storage entry.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public com.google.protobuf.ByteString
+          getValueBytes() {
+        return instance.getValueBytes();
+      }
+      /**
+       * <pre>
+       * The value of the local storage entry.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder setValue(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setValue(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The value of the local storage entry.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder clearValue() {
+        copyOnWrite();
+        instance.clearValue();
+        return this;
+      }
+      /**
+       * <pre>
+       * The value of the local storage entry.
+       * </pre>
+       *
+       * <code>optional string value = 2;</code>
+       */
+      public Builder setValueBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setValueBytes(value);
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:cobalt.storage.LocalStorageEntry)
+    }
+    protected final Object dynamicMethod(
+        com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+        Object arg0, Object arg1) {
+      switch (method) {
+        case NEW_MUTABLE_INSTANCE: {
+          return new dev.cobalt.storage.StorageProto.LocalStorageEntry();
+        }
+        case IS_INITIALIZED: {
+          return DEFAULT_INSTANCE;
+        }
+        case MAKE_IMMUTABLE: {
+          return null;
+        }
+        case NEW_BUILDER: {
+          return new Builder();
+        }
+        case VISIT: {
+          Visitor visitor = (Visitor) arg0;
+          dev.cobalt.storage.StorageProto.LocalStorageEntry other = (dev.cobalt.storage.StorageProto.LocalStorageEntry) arg1;
+          key_ = visitor.visitString(!key_.isEmpty(), key_,
+              !other.key_.isEmpty(), other.key_);
+          value_ = visitor.visitString(!value_.isEmpty(), value_,
+              !other.value_.isEmpty(), other.value_);
+          if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+              .INSTANCE) {
+          }
+          return this;
+        }
+        case MERGE_FROM_STREAM: {
+          com.google.protobuf.CodedInputStream input =
+              (com.google.protobuf.CodedInputStream) arg0;
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+              (com.google.protobuf.ExtensionRegistryLite) arg1;
+          try {
+            boolean done = false;
+            while (!done) {
+              int tag = input.readTag();
+              switch (tag) {
+                case 0:
+                  done = true;
+                  break;
+                default: {
+                  if (!input.skipField(tag)) {
+                    done = true;
+                  }
+                  break;
+                }
+                case 10: {
+                  String s = input.readStringRequireUtf8();
+
+                  key_ = s;
+                  break;
+                }
+                case 18: {
+                  String s = input.readStringRequireUtf8();
+
+                  value_ = s;
+                  break;
+                }
+              }
+            }
+          } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            throw new RuntimeException(e.setUnfinishedMessage(this));
+          } catch (java.io.IOException e) {
+            throw new RuntimeException(
+                new com.google.protobuf.InvalidProtocolBufferException(
+                    e.getMessage()).setUnfinishedMessage(this));
+          } finally {
+          }
+        }
+        case GET_DEFAULT_INSTANCE: {
+          return DEFAULT_INSTANCE;
+        }
+        case GET_PARSER: {
+          if (PARSER == null) {    synchronized (dev.cobalt.storage.StorageProto.LocalStorageEntry.class) {
+              if (PARSER == null) {
+                PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+              }
+            }
+          }
+          return PARSER;
+        }
+      }
+      throw new UnsupportedOperationException();
+    }
+
+
+    // @@protoc_insertion_point(class_scope:cobalt.storage.LocalStorageEntry)
+    private static final dev.cobalt.storage.StorageProto.LocalStorageEntry DEFAULT_INSTANCE;
+    static {
+      DEFAULT_INSTANCE = new LocalStorageEntry();
+      DEFAULT_INSTANCE.makeImmutable();
+    }
+
+    public static dev.cobalt.storage.StorageProto.LocalStorageEntry getDefaultInstance() {
+      return DEFAULT_INSTANCE;
+    }
+
+    private static volatile com.google.protobuf.Parser<LocalStorageEntry> PARSER;
+
+    public static com.google.protobuf.Parser<LocalStorageEntry> parser() {
+      return DEFAULT_INSTANCE.getParserForType();
+    }
+  }
+
+  public interface LocalStorageOrBuilder extends
+      // @@protoc_insertion_point(interface_extends:cobalt.storage.LocalStorage)
+      com.google.protobuf.MessageLiteOrBuilder {
+
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    java.lang.String getSerializedOrigin();
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    com.google.protobuf.ByteString
+        getSerializedOriginBytes();
+
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry> 
+        getLocalStorageEntriesList();
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index);
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    int getLocalStorageEntriesCount();
+  }
+  /**
+   * <pre>
+   * Multiple local storages identified by unique id.
+   * </pre>
+   *
+   * Protobuf type {@code cobalt.storage.LocalStorage}
+   */
+  public  static final class LocalStorage extends
+      com.google.protobuf.GeneratedMessageLite<
+          LocalStorage, LocalStorage.Builder> implements
+      // @@protoc_insertion_point(message_implements:cobalt.storage.LocalStorage)
+      LocalStorageOrBuilder {
+    private LocalStorage() {
+      serializedOrigin_ = "";
+      localStorageEntries_ = emptyProtobufList();
+    }
+    private int bitField0_;
+    public static final int SERIALIZED_ORIGIN_FIELD_NUMBER = 1;
+    private java.lang.String serializedOrigin_;
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    public java.lang.String getSerializedOrigin() {
+      return serializedOrigin_;
+    }
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    public com.google.protobuf.ByteString
+        getSerializedOriginBytes() {
+      return com.google.protobuf.ByteString.copyFromUtf8(serializedOrigin_);
+    }
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    private void setSerializedOrigin(
+        java.lang.String value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  
+      serializedOrigin_ = value;
+    }
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    private void clearSerializedOrigin() {
+      
+      serializedOrigin_ = getDefaultInstance().getSerializedOrigin();
+    }
+    /**
+     * <pre>
+     * A serialzied origin as defined in:
+     * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+     * For example: "https://www.youtube.com"
+     * </pre>
+     *
+     * <code>optional string serialized_origin = 1;</code>
+     */
+    private void setSerializedOriginBytes(
+        com.google.protobuf.ByteString value) {
+      if (value == null) {
+    throw new NullPointerException();
+  }
+  checkByteStringIsUtf8(value);
+      
+      serializedOrigin_ = value.toStringUtf8();
+    }
+
+    public static final int LOCAL_STORAGE_ENTRIES_FIELD_NUMBER = 2;
+    private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.LocalStorageEntry> localStorageEntries_;
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    public java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry> getLocalStorageEntriesList() {
+      return localStorageEntries_;
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    public java.util.List<? extends dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder> 
+        getLocalStorageEntriesOrBuilderList() {
+      return localStorageEntries_;
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    public int getLocalStorageEntriesCount() {
+      return localStorageEntries_.size();
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    public dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index) {
+      return localStorageEntries_.get(index);
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    public dev.cobalt.storage.StorageProto.LocalStorageEntryOrBuilder getLocalStorageEntriesOrBuilder(
+        int index) {
+      return localStorageEntries_.get(index);
+    }
+    private void ensureLocalStorageEntriesIsMutable() {
+      if (!localStorageEntries_.isModifiable()) {
+        localStorageEntries_ =
+            com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorageEntries_);
+       }
+    }
+
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void setLocalStorageEntries(
+        int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.set(index, value);
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void setLocalStorageEntries(
+        int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.set(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void addLocalStorageEntries(dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.add(value);
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void addLocalStorageEntries(
+        int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.add(index, value);
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void addLocalStorageEntries(
+        dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.add(builderForValue.build());
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void addLocalStorageEntries(
+        int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.add(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void addAllLocalStorageEntries(
+        java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorageEntry> values) {
+      ensureLocalStorageEntriesIsMutable();
+      com.google.protobuf.AbstractMessageLite.addAll(
+          values, localStorageEntries_);
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void clearLocalStorageEntries() {
+      localStorageEntries_ = emptyProtobufList();
+    }
+    /**
+     * <pre>
+     * The local storage entries for individual local storage.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+     */
+    private void removeLocalStorageEntries(int index) {
+      ensureLocalStorageEntriesIsMutable();
+      localStorageEntries_.remove(index);
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      if (!serializedOrigin_.isEmpty()) {
+        output.writeString(1, getSerializedOrigin());
+      }
+      for (int i = 0; i < localStorageEntries_.size(); i++) {
+        output.writeMessage(2, localStorageEntries_.get(i));
+      }
+    }
+
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (!serializedOrigin_.isEmpty()) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeStringSize(1, getSerializedOrigin());
+      }
+      for (int i = 0; i < localStorageEntries_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(2, localStorageEntries_.get(i));
+      }
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.LocalStorage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() {
+      return DEFAULT_INSTANCE.toBuilder();
+    }
+    public static Builder newBuilder(dev.cobalt.storage.StorageProto.LocalStorage prototype) {
+      return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+    }
+
+    /**
+     * <pre>
+     * Multiple local storages identified by unique id.
+     * </pre>
+     *
+     * Protobuf type {@code cobalt.storage.LocalStorage}
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessageLite.Builder<
+          dev.cobalt.storage.StorageProto.LocalStorage, Builder> implements
+        // @@protoc_insertion_point(builder_implements:cobalt.storage.LocalStorage)
+        dev.cobalt.storage.StorageProto.LocalStorageOrBuilder {
+      // Construct using dev.cobalt.storage.StorageProto.LocalStorage.newBuilder()
+      private Builder() {
+        super(DEFAULT_INSTANCE);
+      }
+
+
+      /**
+       * <pre>
+       * A serialzied origin as defined in:
+       * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+       * For example: "https://www.youtube.com"
+       * </pre>
+       *
+       * <code>optional string serialized_origin = 1;</code>
+       */
+      public java.lang.String getSerializedOrigin() {
+        return instance.getSerializedOrigin();
+      }
+      /**
+       * <pre>
+       * A serialzied origin as defined in:
+       * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+       * For example: "https://www.youtube.com"
+       * </pre>
+       *
+       * <code>optional string serialized_origin = 1;</code>
+       */
+      public com.google.protobuf.ByteString
+          getSerializedOriginBytes() {
+        return instance.getSerializedOriginBytes();
+      }
+      /**
+       * <pre>
+       * A serialzied origin as defined in:
+       * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+       * For example: "https://www.youtube.com"
+       * </pre>
+       *
+       * <code>optional string serialized_origin = 1;</code>
+       */
+      public Builder setSerializedOrigin(
+          java.lang.String value) {
+        copyOnWrite();
+        instance.setSerializedOrigin(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * A serialzied origin as defined in:
+       * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+       * For example: "https://www.youtube.com"
+       * </pre>
+       *
+       * <code>optional string serialized_origin = 1;</code>
+       */
+      public Builder clearSerializedOrigin() {
+        copyOnWrite();
+        instance.clearSerializedOrigin();
+        return this;
+      }
+      /**
+       * <pre>
+       * A serialzied origin as defined in:
+       * https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin.
+       * For example: "https://www.youtube.com"
+       * </pre>
+       *
+       * <code>optional string serialized_origin = 1;</code>
+       */
+      public Builder setSerializedOriginBytes(
+          com.google.protobuf.ByteString value) {
+        copyOnWrite();
+        instance.setSerializedOriginBytes(value);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public java.util.List<dev.cobalt.storage.StorageProto.LocalStorageEntry> getLocalStorageEntriesList() {
+        return java.util.Collections.unmodifiableList(
+            instance.getLocalStorageEntriesList());
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public int getLocalStorageEntriesCount() {
+        return instance.getLocalStorageEntriesCount();
+      }/**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public dev.cobalt.storage.StorageProto.LocalStorageEntry getLocalStorageEntries(int index) {
+        return instance.getLocalStorageEntries(index);
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder setLocalStorageEntries(
+          int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+        copyOnWrite();
+        instance.setLocalStorageEntries(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder setLocalStorageEntries(
+          int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+        copyOnWrite();
+        instance.setLocalStorageEntries(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder addLocalStorageEntries(dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+        copyOnWrite();
+        instance.addLocalStorageEntries(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder addLocalStorageEntries(
+          int index, dev.cobalt.storage.StorageProto.LocalStorageEntry value) {
+        copyOnWrite();
+        instance.addLocalStorageEntries(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder addLocalStorageEntries(
+          dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+        copyOnWrite();
+        instance.addLocalStorageEntries(builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder addLocalStorageEntries(
+          int index, dev.cobalt.storage.StorageProto.LocalStorageEntry.Builder builderForValue) {
+        copyOnWrite();
+        instance.addLocalStorageEntries(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder addAllLocalStorageEntries(
+          java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorageEntry> values) {
+        copyOnWrite();
+        instance.addAllLocalStorageEntries(values);
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder clearLocalStorageEntries() {
+        copyOnWrite();
+        instance.clearLocalStorageEntries();
+        return this;
+      }
+      /**
+       * <pre>
+       * The local storage entries for individual local storage.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorageEntry local_storage_entries = 2;</code>
+       */
+      public Builder removeLocalStorageEntries(int index) {
+        copyOnWrite();
+        instance.removeLocalStorageEntries(index);
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:cobalt.storage.LocalStorage)
+    }
+    protected final Object dynamicMethod(
+        com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+        Object arg0, Object arg1) {
+      switch (method) {
+        case NEW_MUTABLE_INSTANCE: {
+          return new dev.cobalt.storage.StorageProto.LocalStorage();
+        }
+        case IS_INITIALIZED: {
+          return DEFAULT_INSTANCE;
+        }
+        case MAKE_IMMUTABLE: {
+          localStorageEntries_.makeImmutable();
+          return null;
+        }
+        case NEW_BUILDER: {
+          return new Builder();
+        }
+        case VISIT: {
+          Visitor visitor = (Visitor) arg0;
+          dev.cobalt.storage.StorageProto.LocalStorage other = (dev.cobalt.storage.StorageProto.LocalStorage) arg1;
+          serializedOrigin_ = visitor.visitString(!serializedOrigin_.isEmpty(), serializedOrigin_,
+              !other.serializedOrigin_.isEmpty(), other.serializedOrigin_);
+          localStorageEntries_= visitor.visitList(localStorageEntries_, other.localStorageEntries_);
+          if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+              .INSTANCE) {
+            bitField0_ |= other.bitField0_;
+          }
+          return this;
+        }
+        case MERGE_FROM_STREAM: {
+          com.google.protobuf.CodedInputStream input =
+              (com.google.protobuf.CodedInputStream) arg0;
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+              (com.google.protobuf.ExtensionRegistryLite) arg1;
+          try {
+            boolean done = false;
+            while (!done) {
+              int tag = input.readTag();
+              switch (tag) {
+                case 0:
+                  done = true;
+                  break;
+                default: {
+                  if (!input.skipField(tag)) {
+                    done = true;
+                  }
+                  break;
+                }
+                case 10: {
+                  String s = input.readStringRequireUtf8();
+
+                  serializedOrigin_ = s;
+                  break;
+                }
+                case 18: {
+                  if (!localStorageEntries_.isModifiable()) {
+                    localStorageEntries_ =
+                        com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorageEntries_);
+                  }
+                  localStorageEntries_.add(
+                      input.readMessage(dev.cobalt.storage.StorageProto.LocalStorageEntry.parser(), extensionRegistry));
+                  break;
+                }
+              }
+            }
+          } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            throw new RuntimeException(e.setUnfinishedMessage(this));
+          } catch (java.io.IOException e) {
+            throw new RuntimeException(
+                new com.google.protobuf.InvalidProtocolBufferException(
+                    e.getMessage()).setUnfinishedMessage(this));
+          } finally {
+          }
+        }
+        case GET_DEFAULT_INSTANCE: {
+          return DEFAULT_INSTANCE;
+        }
+        case GET_PARSER: {
+          if (PARSER == null) {    synchronized (dev.cobalt.storage.StorageProto.LocalStorage.class) {
+              if (PARSER == null) {
+                PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+              }
+            }
+          }
+          return PARSER;
+        }
+      }
+      throw new UnsupportedOperationException();
+    }
+
+
+    // @@protoc_insertion_point(class_scope:cobalt.storage.LocalStorage)
+    private static final dev.cobalt.storage.StorageProto.LocalStorage DEFAULT_INSTANCE;
+    static {
+      DEFAULT_INSTANCE = new LocalStorage();
+      DEFAULT_INSTANCE.makeImmutable();
+    }
+
+    public static dev.cobalt.storage.StorageProto.LocalStorage getDefaultInstance() {
+      return DEFAULT_INSTANCE;
+    }
+
+    private static volatile com.google.protobuf.Parser<LocalStorage> PARSER;
+
+    public static com.google.protobuf.Parser<LocalStorage> parser() {
+      return DEFAULT_INSTANCE.getParserForType();
+    }
+  }
+
+  public interface StorageOrBuilder extends
+      // @@protoc_insertion_point(interface_extends:cobalt.storage.Storage)
+      com.google.protobuf.MessageLiteOrBuilder {
+
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    java.util.List<dev.cobalt.storage.StorageProto.Cookie> 
+        getCookiesList();
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    dev.cobalt.storage.StorageProto.Cookie getCookies(int index);
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    int getCookiesCount();
+
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    java.util.List<dev.cobalt.storage.StorageProto.LocalStorage> 
+        getLocalStoragesList();
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index);
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    int getLocalStoragesCount();
+  }
+  /**
+   * <pre>
+   * The full storage.
+   * </pre>
+   *
+   * Protobuf type {@code cobalt.storage.Storage}
+   */
+  public  static final class Storage extends
+      com.google.protobuf.GeneratedMessageLite<
+          Storage, Storage.Builder> implements
+      // @@protoc_insertion_point(message_implements:cobalt.storage.Storage)
+      StorageOrBuilder {
+    private Storage() {
+      cookies_ = emptyProtobufList();
+      localStorages_ = emptyProtobufList();
+    }
+    public static final int COOKIES_FIELD_NUMBER = 1;
+    private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.Cookie> cookies_;
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    public java.util.List<dev.cobalt.storage.StorageProto.Cookie> getCookiesList() {
+      return cookies_;
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    public java.util.List<? extends dev.cobalt.storage.StorageProto.CookieOrBuilder> 
+        getCookiesOrBuilderList() {
+      return cookies_;
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    public int getCookiesCount() {
+      return cookies_.size();
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    public dev.cobalt.storage.StorageProto.Cookie getCookies(int index) {
+      return cookies_.get(index);
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    public dev.cobalt.storage.StorageProto.CookieOrBuilder getCookiesOrBuilder(
+        int index) {
+      return cookies_.get(index);
+    }
+    private void ensureCookiesIsMutable() {
+      if (!cookies_.isModifiable()) {
+        cookies_ =
+            com.google.protobuf.GeneratedMessageLite.mutableCopy(cookies_);
+       }
+    }
+
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void setCookies(
+        int index, dev.cobalt.storage.StorageProto.Cookie value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureCookiesIsMutable();
+      cookies_.set(index, value);
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void setCookies(
+        int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+      ensureCookiesIsMutable();
+      cookies_.set(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void addCookies(dev.cobalt.storage.StorageProto.Cookie value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureCookiesIsMutable();
+      cookies_.add(value);
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void addCookies(
+        int index, dev.cobalt.storage.StorageProto.Cookie value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureCookiesIsMutable();
+      cookies_.add(index, value);
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void addCookies(
+        dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+      ensureCookiesIsMutable();
+      cookies_.add(builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void addCookies(
+        int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+      ensureCookiesIsMutable();
+      cookies_.add(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void addAllCookies(
+        java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.Cookie> values) {
+      ensureCookiesIsMutable();
+      com.google.protobuf.AbstractMessageLite.addAll(
+          values, cookies_);
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void clearCookies() {
+      cookies_ = emptyProtobufList();
+    }
+    /**
+     * <pre>
+     * All the cookies.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+     */
+    private void removeCookies(int index) {
+      ensureCookiesIsMutable();
+      cookies_.remove(index);
+    }
+
+    public static final int LOCAL_STORAGES_FIELD_NUMBER = 2;
+    private com.google.protobuf.Internal.ProtobufList<dev.cobalt.storage.StorageProto.LocalStorage> localStorages_;
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    public java.util.List<dev.cobalt.storage.StorageProto.LocalStorage> getLocalStoragesList() {
+      return localStorages_;
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    public java.util.List<? extends dev.cobalt.storage.StorageProto.LocalStorageOrBuilder> 
+        getLocalStoragesOrBuilderList() {
+      return localStorages_;
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    public int getLocalStoragesCount() {
+      return localStorages_.size();
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    public dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index) {
+      return localStorages_.get(index);
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    public dev.cobalt.storage.StorageProto.LocalStorageOrBuilder getLocalStoragesOrBuilder(
+        int index) {
+      return localStorages_.get(index);
+    }
+    private void ensureLocalStoragesIsMutable() {
+      if (!localStorages_.isModifiable()) {
+        localStorages_ =
+            com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorages_);
+       }
+    }
+
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void setLocalStorages(
+        int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStoragesIsMutable();
+      localStorages_.set(index, value);
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void setLocalStorages(
+        int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+      ensureLocalStoragesIsMutable();
+      localStorages_.set(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void addLocalStorages(dev.cobalt.storage.StorageProto.LocalStorage value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStoragesIsMutable();
+      localStorages_.add(value);
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void addLocalStorages(
+        int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+      if (value == null) {
+        throw new NullPointerException();
+      }
+      ensureLocalStoragesIsMutable();
+      localStorages_.add(index, value);
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void addLocalStorages(
+        dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+      ensureLocalStoragesIsMutable();
+      localStorages_.add(builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void addLocalStorages(
+        int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+      ensureLocalStoragesIsMutable();
+      localStorages_.add(index, builderForValue.build());
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void addAllLocalStorages(
+        java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorage> values) {
+      ensureLocalStoragesIsMutable();
+      com.google.protobuf.AbstractMessageLite.addAll(
+          values, localStorages_);
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void clearLocalStorages() {
+      localStorages_ = emptyProtobufList();
+    }
+    /**
+     * <pre>
+     * All local storages.
+     * </pre>
+     *
+     * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+     */
+    private void removeLocalStorages(int index) {
+      ensureLocalStoragesIsMutable();
+      localStorages_.remove(index);
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      for (int i = 0; i < cookies_.size(); i++) {
+        output.writeMessage(1, cookies_.get(i));
+      }
+      for (int i = 0; i < localStorages_.size(); i++) {
+        output.writeMessage(2, localStorages_.get(i));
+      }
+    }
+
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      for (int i = 0; i < cookies_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(1, cookies_.get(i));
+      }
+      for (int i = 0; i < localStorages_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(2, localStorages_.get(i));
+      }
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, data, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input);
+    }
+    public static dev.cobalt.storage.StorageProto.Storage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return com.google.protobuf.GeneratedMessageLite.parseFrom(
+          DEFAULT_INSTANCE, input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() {
+      return DEFAULT_INSTANCE.toBuilder();
+    }
+    public static Builder newBuilder(dev.cobalt.storage.StorageProto.Storage prototype) {
+      return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+    }
+
+    /**
+     * <pre>
+     * The full storage.
+     * </pre>
+     *
+     * Protobuf type {@code cobalt.storage.Storage}
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessageLite.Builder<
+          dev.cobalt.storage.StorageProto.Storage, Builder> implements
+        // @@protoc_insertion_point(builder_implements:cobalt.storage.Storage)
+        dev.cobalt.storage.StorageProto.StorageOrBuilder {
+      // Construct using dev.cobalt.storage.StorageProto.Storage.newBuilder()
+      private Builder() {
+        super(DEFAULT_INSTANCE);
+      }
+
+
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public java.util.List<dev.cobalt.storage.StorageProto.Cookie> getCookiesList() {
+        return java.util.Collections.unmodifiableList(
+            instance.getCookiesList());
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public int getCookiesCount() {
+        return instance.getCookiesCount();
+      }/**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public dev.cobalt.storage.StorageProto.Cookie getCookies(int index) {
+        return instance.getCookies(index);
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder setCookies(
+          int index, dev.cobalt.storage.StorageProto.Cookie value) {
+        copyOnWrite();
+        instance.setCookies(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder setCookies(
+          int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+        copyOnWrite();
+        instance.setCookies(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder addCookies(dev.cobalt.storage.StorageProto.Cookie value) {
+        copyOnWrite();
+        instance.addCookies(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder addCookies(
+          int index, dev.cobalt.storage.StorageProto.Cookie value) {
+        copyOnWrite();
+        instance.addCookies(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder addCookies(
+          dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+        copyOnWrite();
+        instance.addCookies(builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder addCookies(
+          int index, dev.cobalt.storage.StorageProto.Cookie.Builder builderForValue) {
+        copyOnWrite();
+        instance.addCookies(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder addAllCookies(
+          java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.Cookie> values) {
+        copyOnWrite();
+        instance.addAllCookies(values);
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder clearCookies() {
+        copyOnWrite();
+        instance.clearCookies();
+        return this;
+      }
+      /**
+       * <pre>
+       * All the cookies.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.Cookie cookies = 1;</code>
+       */
+      public Builder removeCookies(int index) {
+        copyOnWrite();
+        instance.removeCookies(index);
+        return this;
+      }
+
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public java.util.List<dev.cobalt.storage.StorageProto.LocalStorage> getLocalStoragesList() {
+        return java.util.Collections.unmodifiableList(
+            instance.getLocalStoragesList());
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public int getLocalStoragesCount() {
+        return instance.getLocalStoragesCount();
+      }/**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public dev.cobalt.storage.StorageProto.LocalStorage getLocalStorages(int index) {
+        return instance.getLocalStorages(index);
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder setLocalStorages(
+          int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+        copyOnWrite();
+        instance.setLocalStorages(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder setLocalStorages(
+          int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+        copyOnWrite();
+        instance.setLocalStorages(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder addLocalStorages(dev.cobalt.storage.StorageProto.LocalStorage value) {
+        copyOnWrite();
+        instance.addLocalStorages(value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder addLocalStorages(
+          int index, dev.cobalt.storage.StorageProto.LocalStorage value) {
+        copyOnWrite();
+        instance.addLocalStorages(index, value);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder addLocalStorages(
+          dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+        copyOnWrite();
+        instance.addLocalStorages(builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder addLocalStorages(
+          int index, dev.cobalt.storage.StorageProto.LocalStorage.Builder builderForValue) {
+        copyOnWrite();
+        instance.addLocalStorages(index, builderForValue);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder addAllLocalStorages(
+          java.lang.Iterable<? extends dev.cobalt.storage.StorageProto.LocalStorage> values) {
+        copyOnWrite();
+        instance.addAllLocalStorages(values);
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder clearLocalStorages() {
+        copyOnWrite();
+        instance.clearLocalStorages();
+        return this;
+      }
+      /**
+       * <pre>
+       * All local storages.
+       * </pre>
+       *
+       * <code>repeated .cobalt.storage.LocalStorage local_storages = 2;</code>
+       */
+      public Builder removeLocalStorages(int index) {
+        copyOnWrite();
+        instance.removeLocalStorages(index);
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:cobalt.storage.Storage)
+    }
+    protected final Object dynamicMethod(
+        com.google.protobuf.GeneratedMessageLite.MethodToInvoke method,
+        Object arg0, Object arg1) {
+      switch (method) {
+        case NEW_MUTABLE_INSTANCE: {
+          return new dev.cobalt.storage.StorageProto.Storage();
+        }
+        case IS_INITIALIZED: {
+          return DEFAULT_INSTANCE;
+        }
+        case MAKE_IMMUTABLE: {
+          cookies_.makeImmutable();
+          localStorages_.makeImmutable();
+          return null;
+        }
+        case NEW_BUILDER: {
+          return new Builder();
+        }
+        case VISIT: {
+          Visitor visitor = (Visitor) arg0;
+          dev.cobalt.storage.StorageProto.Storage other = (dev.cobalt.storage.StorageProto.Storage) arg1;
+          cookies_= visitor.visitList(cookies_, other.cookies_);
+          localStorages_= visitor.visitList(localStorages_, other.localStorages_);
+          if (visitor == com.google.protobuf.GeneratedMessageLite.MergeFromVisitor
+              .INSTANCE) {
+          }
+          return this;
+        }
+        case MERGE_FROM_STREAM: {
+          com.google.protobuf.CodedInputStream input =
+              (com.google.protobuf.CodedInputStream) arg0;
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry =
+              (com.google.protobuf.ExtensionRegistryLite) arg1;
+          try {
+            boolean done = false;
+            while (!done) {
+              int tag = input.readTag();
+              switch (tag) {
+                case 0:
+                  done = true;
+                  break;
+                default: {
+                  if (!input.skipField(tag)) {
+                    done = true;
+                  }
+                  break;
+                }
+                case 10: {
+                  if (!cookies_.isModifiable()) {
+                    cookies_ =
+                        com.google.protobuf.GeneratedMessageLite.mutableCopy(cookies_);
+                  }
+                  cookies_.add(
+                      input.readMessage(dev.cobalt.storage.StorageProto.Cookie.parser(), extensionRegistry));
+                  break;
+                }
+                case 18: {
+                  if (!localStorages_.isModifiable()) {
+                    localStorages_ =
+                        com.google.protobuf.GeneratedMessageLite.mutableCopy(localStorages_);
+                  }
+                  localStorages_.add(
+                      input.readMessage(dev.cobalt.storage.StorageProto.LocalStorage.parser(), extensionRegistry));
+                  break;
+                }
+              }
+            }
+          } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+            throw new RuntimeException(e.setUnfinishedMessage(this));
+          } catch (java.io.IOException e) {
+            throw new RuntimeException(
+                new com.google.protobuf.InvalidProtocolBufferException(
+                    e.getMessage()).setUnfinishedMessage(this));
+          } finally {
+          }
+        }
+        case GET_DEFAULT_INSTANCE: {
+          return DEFAULT_INSTANCE;
+        }
+        case GET_PARSER: {
+          if (PARSER == null) {    synchronized (dev.cobalt.storage.StorageProto.Storage.class) {
+              if (PARSER == null) {
+                PARSER = new DefaultInstanceBasedParser(DEFAULT_INSTANCE);
+              }
+            }
+          }
+          return PARSER;
+        }
+      }
+      throw new UnsupportedOperationException();
+    }
+
+
+    // @@protoc_insertion_point(class_scope:cobalt.storage.Storage)
+    private static final dev.cobalt.storage.StorageProto.Storage DEFAULT_INSTANCE;
+    static {
+      DEFAULT_INSTANCE = new Storage();
+      DEFAULT_INSTANCE.makeImmutable();
+    }
+
+    public static dev.cobalt.storage.StorageProto.Storage getDefaultInstance() {
+      return DEFAULT_INSTANCE;
+    }
+
+    private static volatile com.google.protobuf.Parser<Storage> PARSER;
+
+    public static com.google.protobuf.Parser<Storage> parser() {
+      return DEFAULT_INSTANCE.getParserForType();
+    }
+  }
+
+
+  static {
+  }
+
+  // @@protoc_insertion_point(outer_class_scope)
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
new file mode 100644
index 0000000..16f2b46
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java
@@ -0,0 +1,82 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.Size;
+import android.view.WindowManager;
+
+/** Utility functions for querying display attributes. */
+public class DisplayUtil {
+
+  private DisplayUtil() {}
+
+  /**
+   * Returns the size of the physical display size in pixels.
+   *
+   * <p>This differs from {@link #getSystemDisplaySize(Context)} because it only uses
+   * {@link DisplayMetrics}.
+   */
+  public static Size getDisplaySize(Context context) {
+    DisplayMetrics metrics = getDisplayMetrics(context);
+    return new Size(metrics.widthPixels, metrics.heightPixels);
+  }
+
+  /**
+   * Returns the size of the current physical display size in pixels.
+   *
+   * <p>This differs from {@link #getDisplaySize(Context)} because it allows the
+   * system property "sys.display-size" to override {@link DisplayMetrics}.
+   */
+  public static Size getSystemDisplaySize(Context context) {
+    Size widthAndHeightPx = getSystemDisplayWidthAndHeightPxInternal();
+    if (widthAndHeightPx == null) {
+      widthAndHeightPx = getDisplaySize(context);
+    }
+    return widthAndHeightPx;
+  }
+
+  /**
+   * Returns the size of the current physical display size in pixels.
+   * or {@code null} if unavailable.
+   */
+  private static Size getSystemDisplayWidthAndHeightPxInternal() {
+    final String displaySize = SystemPropertiesHelper.getString("sys.display-size");
+    if (displaySize != null) {
+      final String[] widthAndHeightPx = displaySize.split("x");
+      if (widthAndHeightPx.length == 2) {
+        try {
+          return new Size(
+              Integer.parseInt(widthAndHeightPx[0]), Integer.parseInt(widthAndHeightPx[1]));
+        } catch (NumberFormatException exception) {
+          // pass
+        }
+      }
+    }
+    return null;
+  }
+
+  private static DisplayMetrics cachedDisplayMetrics = null;
+
+  private static DisplayMetrics getDisplayMetrics(Context context) {
+    if (cachedDisplayMetrics == null) {
+      cachedDisplayMetrics = new DisplayMetrics();
+      ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+          .getDefaultDisplay().getRealMetrics(cachedDisplayMetrics);
+    }
+    return cachedDisplayMetrics;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java
new file mode 100644
index 0000000..7fa3407
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Holder.java
@@ -0,0 +1,35 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.support.annotation.Nullable;
+
+/** Holds a mutable reference to an object, or null. */
+public class Holder<T> {
+  private T instance;
+
+  public Holder() {
+    this.instance = null;
+  }
+
+  public void set(@Nullable T instance) {
+    this.instance = instance;
+  }
+
+  @Nullable
+  public T get() {
+    return instance;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java
new file mode 100644
index 0000000..0ade400
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/IsEmulator.java
@@ -0,0 +1,29 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import android.os.Build;
+
+/** A simple utility class to detect whether we're running in an emulator or not. */
+public class IsEmulator {
+  private IsEmulator() {}
+
+  public static boolean isEmulator() {
+    String qemu = System.getProperty("ro.kernel.qemu", "?");
+    return qemu.equals("1")
+        || Build.HARDWARE.contains("goldfish")
+        || Build.HARDWARE.contains("ranchu");
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
new file mode 100644
index 0000000..59af6ac
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/Log.java
@@ -0,0 +1,71 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+/**
+ * API for sending Starboard log output. This uses a JNI helper rather than directly calling Android
+ * logging so that it remains in the app even when Android logging is stripped by Proguard.
+ */
+public final class Log {
+  public static final String TAG = "starboard";
+
+  private Log() {}
+
+  private static native int nativeWrite(char priority, String tag, String msg, Throwable tr);
+
+  public static int v(String tag, String msg) {
+    return nativeWrite('v', tag, msg, null);
+  }
+
+  public static int v(String tag, String msg, Throwable tr) {
+    return nativeWrite('v', tag, msg, tr);
+  }
+
+  public static int d(String tag, String msg) {
+    return nativeWrite('d', tag, msg, null);
+  }
+
+  public static int d(String tag, String msg, Throwable tr) {
+    return nativeWrite('d', tag, msg, tr);
+  }
+
+  public static int i(String tag, String msg) {
+    return nativeWrite('i', tag, msg, null);
+  }
+
+  public static int i(String tag, String msg, Throwable tr) {
+    return nativeWrite('i', tag, msg, tr);
+  }
+
+  public static int w(String tag, String msg) {
+    return nativeWrite('w', tag, msg, null);
+  }
+
+  public static int w(String tag, String msg, Throwable tr) {
+    return nativeWrite('w', tag, msg, tr);
+  }
+
+  public static int w(String tag, Throwable tr) {
+    return nativeWrite('w', tag, "", tr);
+  }
+
+  public static int e(String tag, String msg) {
+    return nativeWrite('e', tag, msg, null);
+  }
+
+  public static int e(String tag, String msg, Throwable tr) {
+    return nativeWrite('e', tag, msg, tr);
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java
new file mode 100644
index 0000000..90448c8
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import static dev.cobalt.util.Log.TAG;
+
+import java.lang.reflect.Method;
+
+/** Utility class for accessing system properties via reflection. */
+public class SystemPropertiesHelper {
+  private static Method getStringMethod;
+  static {
+    try {
+      getStringMethod = ClassLoader.getSystemClassLoader()
+          .loadClass("android.os.SystemProperties")
+          .getMethod("get", String.class);
+      if (getStringMethod == null) {
+        Log.e(TAG, "Couldn't load system properties getString");
+      }
+    } catch (Exception exception) {
+      Log.e(TAG, "Exception looking up system properties methods: ", exception);
+    }
+  }
+
+  private SystemPropertiesHelper() {}
+
+  public static String getString(String property) {
+    if (getStringMethod != null) {
+      try {
+        return (String) getStringMethod.invoke(null, new Object[] { property });
+      } catch (Exception exception) {
+        Log.e(TAG, "Exception getting system property: ", exception);
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java
new file mode 100644
index 0000000..d78647d
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/util/UsedByNative.java
@@ -0,0 +1,25 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cobalt.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used for marking methods and fields that are called from native code. Useful for
+ * keeping components that would otherwise be removed by Proguard.
+ */
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR})
+public @interface UsedByNative {}
diff --git a/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml b/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml
new file mode 100644
index 0000000..c3f6390
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/layout/coat_error_dialog.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/error_frame"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:descendantFocusability="afterDescendants"
+    android:nextFocusLeft="@id/error_frame"
+    android:nextFocusRight="@id/error_frame"
+    android:nextFocusUp="@id/error_frame"
+    android:nextFocusDown="@id/error_frame">
+
+  <LinearLayout
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"
+      android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/image"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/lb_error_image_max_height"
+        android:layout_gravity="center" />
+    <TextView
+        android:id="@+id/message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:maxWidth="@dimen/lb_error_message_max_width"
+        style="?attr/errorMessageStyle"/>
+    <Button
+        android:id="@+id/button_1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:visibility="gone"
+        style="?android:attr/buttonStyle"/>
+    <Button
+        android:id="@+id/button_2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:visibility="gone"
+        style="?android:attr/buttonStyle"/>
+    <Button
+        android:id="@+id/button_3"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:visibility="gone"
+        style="?android:attr/buttonStyle"/>
+  </LinearLayout>
+
+</FrameLayout>
diff --git a/src/starboard/android/apk/app/src/main/res/values/colors.xml b/src/starboard/android/apk/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..30fde0c
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+  <color name="cobalt_blue">#ff16396b</color>
+  <color name="primary">@color/cobalt_blue</color>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/ids.xml b/src/starboard/android/apk/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..8097776
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2017 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+  <item name="rc_choose_account" type="id"/>
+  <item name="rc_get_accounts_permission" type="id"/>
+  <item name="rc_record_audio" type="id"/>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/strings.xml b/src/starboard/android/apk/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8b7369f
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+  <!-- Error message when there's a problem connecting to the network. -->
+  <string name="starboard_platform_connection_error">Can\'t connect right now</string>
+  <!-- Button label on network connection error to retry the request. -->
+  <string name="starboard_platform_retry">Try again</string>
+  <!-- Button label on network connection error to open system network settings. -->
+  <string name="starboard_platform_network_settings">Open network settings</string>
+  <!-- Toast message when we can't get the device account to sign in. -->
+  <string name="starboard_account_picker_error">Account not available.</string>
+  <!-- Toast message when we the account can't be authorized. -->
+  <string name="starboard_account_auth_error">Account authorization failed.</string>
+  <!-- Toast message indicating that the selected account is no longer available to sign-in. -->
+  <string name="starboard_missing_account">Account not available:\n%1$s</string>
+  <!-- Toast message shown when the user has denied permission to get device accounts. -->
+  <string name="starboard_accounts_permission">Sign in requires Contacts permission to be granted in system settings.</string>
+</resources>
diff --git a/src/starboard/android/apk/app/src/main/res/values/styles.xml b/src/starboard/android/apk/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ef05d47
--- /dev/null
+++ b/src/starboard/android/apk/app/src/main/res/values/styles.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2016 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+  <style name="CobaltTheme" parent="@style/android:Theme.Material.NoActionBar">
+    <!-- Color of the transition animation when launching the app. -->
+    <item name="android:colorPrimary">@color/primary</item>
+
+    <!-- Color shown after the transition before the window is attached.
+         Avoids a black flash between the transition and splash screen. -->
+    <item name="android:windowBackground">?android:colorPrimary</item>
+
+    <item name="android:dialogTheme">@style/FullscreenDialogTheme</item>
+  </style>
+
+  <style name="FullscreenDialogTheme" parent="Theme.Leanback">
+    <item name="android:layout_width">fill_parent</item>
+    <item name="android:layout_height">fill_parent</item>
+    <item name="android:windowIsTranslucent">true</item>
+    <item name="android:windowBackground">@color/lb_error_background_color_translucent</item>
+    <item name="android:windowNoTitle">true</item>
+    <item name="android:windowIsFloating">false</item>
+    <item name="android:windowOverscan">true</item>
+    <item name="android:backgroundDimEnabled">false</item>
+    <item name="errorMessageStyle">@style/ErrorMessageStyle</item>
+    <!-- Make the buttons darker so that you can see the focus highlight on the dark background. -->
+    <item name="android:colorButtonNormal">#282828</item>
+  </style>
+
+  <style name="ErrorMessageStyle" parent="Widget.Leanback.ErrorMessageStyle">
+    <item name="android:layout_margin">16dp</item>
+  </style>
+</resources>
diff --git a/src/starboard/android/apk/build.gradle b/src/starboard/android/apk/build.gradle
new file mode 100644
index 0000000..b50ae22
--- /dev/null
+++ b/src/starboard/android/apk/build.gradle
@@ -0,0 +1,55 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+    gradle.projectsEvaluated {
+        tasks.withType(JavaCompile) {
+            options.compilerArgs += [
+                    "-Xlint:unchecked",
+                    "-Xlint:deprecation",
+                    ]
+        }
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
+
+// Move the 'buildDir' for all projects into sub-directories of a shared top-level build directory,
+// which is either the root's original 'buildDir' or a custom location when building for platform
+// deploy.  Note that the platform deploy action sets a custom 'cobaltGradleDir' property rather
+// than setting 'buildDir' directly on the command line since Gradle trys to get smart about
+// 'buildDir' which can end up putting it at the wrong depth in the file system.
+def rootBuildDir = hasProperty('cobaltGradleDir') ? new File(cobaltGradleDir, 'build') : buildDir
+allprojects { buildDir = new File(rootBuildDir, project.name).canonicalFile }
diff --git a/src/starboard/android/apk/cobalt-gradle.sh b/src/starboard/android/apk/cobalt-gradle.sh
new file mode 100755
index 0000000..d135b4b
--- /dev/null
+++ b/src/starboard/android/apk/cobalt-gradle.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Helper to set ANDROID_HOME and ANDROID_NDK_HOME from command-line args
+# before running gradlew, as specified by leading --sdk and --ndk args.
+#
+# Also resets hung gradle builds when specified by a leading --reset arg.
+
+GRADLE_ARGS=()
+while [ "$1" ]; do
+  case "$1" in
+    --sdk) shift; ANDROID_HOME="$1" ;;
+    --ndk) shift; ANDROID_NDK_HOME="$1" ;;
+    --cache) shift; mkdir -p "$1";
+             GRADLE_ARGS+=("--project-cache-dir" $(cd "$1"; pwd)) ;;
+    --reset) RESET_GRADLE=1 ;;
+    *) break ;;
+  esac
+  shift
+done
+GRADLE_ARGS+=("$@")
+
+# Cleanup Gradle from previous builds. Used as part of the GYP step.
+if [[ "${RESET_GRADLE}" ]]; then
+  echo "Cleaning Gradle deamons and locks."
+  # If there are any lock files, kill any hung processes still waiting on them.
+  if compgen -G '/var/lock/cobalt-gradle.lock.*'; then
+    lsof -t /var/lock/cobalt-gradle.lock.* | xargs -rt kill
+  fi
+  # Stop the Gradle daemon (if still running).
+  $(dirname "$0")/gradlew --stop
+  # Remove Gradle caches (including its lock files).
+  rm -rf ${HOME}/.gradle/caches
+  # Show the gradle version, which will cause it to download if needed.
+  $(dirname "$0")/gradlew -v
+  # After resetting, exit without running any gradle tasks.
+  exit
+fi
+
+export ANDROID_HOME
+export ANDROID_NDK_HOME
+echo "ANDROID_HOME=${ANDROID_HOME}"
+echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}"
+echo "TASK: ${GRADLE_ARGS[-1]}"
+
+# Allow parallel gradle builds, as defined by a COBALT_GRADLE_BUILD_COUNT envvar
+# or default to 1 if that's not set (so buildbot only runs 1 gradle at a time).
+BUCKETS=${COBALT_GRADLE_BUILD_COUNT:-1}
+MD5=$(echo "${GRADLE_ARGS[@]}" | md5sum)
+LOCKNUM=$(( ${BUCKETS} * 0x${MD5:0:6} / 0x1000000 ))
+
+flock /var/lock/cobalt-gradle.lock.${LOCKNUM} $(dirname "$0")/gradlew "${GRADLE_ARGS[@]}"
diff --git a/src/starboard/android/apk/gradle.properties b/src/starboard/android/apk/gradle.properties
new file mode 100644
index 0000000..2d5bfe3
--- /dev/null
+++ b/src/starboard/android/apk/gradle.properties
@@ -0,0 +1,31 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx4g
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b17c1fc
--- /dev/null
+++ b/src/starboard/android/apk/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 29 11:03:00 PST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/src/starboard/android/apk/gradlew b/src/starboard/android/apk/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/src/starboard/android/apk/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/src/starboard/android/apk/gradlew.bat b/src/starboard/android/apk/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/src/starboard/android/apk/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/src/starboard/android/apk/settings.gradle b/src/starboard/android/apk/settings.gradle
new file mode 100644
index 0000000..1abb6c2
--- /dev/null
+++ b/src/starboard/android/apk/settings.gradle
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+include ':app'
diff --git a/src/starboard/android/arm/atomic_public.h b/src/starboard/android/arm/atomic_public.h
new file mode 100644
index 0000000..57fad75
--- /dev/null
+++ b/src/starboard/android/arm/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/arm/configuration_public.h b/src/starboard/android/arm/configuration_public.h
new file mode 100644
index 0000000..aa26c16
--- /dev/null
+++ b/src/starboard/android/arm/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android ARM. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/arm/gyp_configuration.gypi b/src/starboard/android/arm/gyp_configuration.gypi
new file mode 100644
index 0000000..d3302b1
--- /dev/null
+++ b/src/starboard/android/arm/gyp_configuration.gypi
@@ -0,0 +1,53 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+  'variables': {
+    'target_arch': 'arm',
+    'arm_version': 7,
+    'armv7': 1,
+    'arm_thumb': 0,
+    'arm_neon': 0,
+    'arm_fpu': 'vfpv3-d16',
+    'compiler_flags': [
+      '-march=armv7-a',
+    ],
+    'linker_flags': [
+      # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+      '-Wl,--exclude-libs,libunwind.a',
+    ]
+  },
+
+  'target_defaults': {
+    'default_configuration': 'android-arm_debug',
+    'configurations': {
+      'android-arm_debug': {
+        'inherit_from': ['debug_base'],
+      },
+      'android-arm_devel': {
+        'inherit_from': ['devel_base'],
+      },
+      'android-arm_qa': {
+        'inherit_from': ['qa_base'],
+      },
+      'android-arm_gold': {
+        'inherit_from': ['gold_base'],
+      },
+    }, # end of configurations
+  },
+
+  'includes': [
+    '../shared/gyp_configuration.gypi',
+  ],
+}
diff --git a/src/starboard/android/arm/gyp_configuration.py b/src/starboard/android/arm/gyp_configuration.py
new file mode 100644
index 0000000..b804d2f
--- /dev/null
+++ b/src/starboard/android/arm/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android ARM platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+  return shared_configuration.AndroidConfiguration('android-arm', 'armeabi-v7a')
diff --git a/src/starboard/android/arm/starboard_platform.gyp b/src/starboard/android/arm/starboard_platform.gyp
new file mode 100644
index 0000000..5110292
--- /dev/null
+++ b/src/starboard/android/arm/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/arm/starboard_platform_tests.gyp b/src/starboard/android/arm/starboard_platform_tests.gyp
new file mode 100644
index 0000000..58cd108
--- /dev/null
+++ b/src/starboard/android/arm/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/arm/thread_types_public.h b/src/starboard/android/arm/thread_types_public.h
new file mode 100644
index 0000000..0b84b59
--- /dev/null
+++ b/src/starboard/android/arm/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/android/arm64/atomic_public.h b/src/starboard/android/arm64/atomic_public.h
new file mode 100644
index 0000000..57fad75
--- /dev/null
+++ b/src/starboard/android/arm64/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/arm64/configuration_public.h b/src/starboard/android/arm64/configuration_public.h
new file mode 100644
index 0000000..52f31dc
--- /dev/null
+++ b/src/starboard/android/arm64/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android ARM-64. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/arm64/gyp_configuration.gypi b/src/starboard/android/arm64/gyp_configuration.gypi
new file mode 100644
index 0000000..cc3a2b5
--- /dev/null
+++ b/src/starboard/android/arm64/gyp_configuration.gypi
@@ -0,0 +1,50 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+  'variables': {
+    'target_arch': 'arm64',
+    'arm_version': 8,
+    'armv7': 0,
+    'arm_thumb': 0,
+    'arm_neon': 0,
+    'arm_fpu': 'vfpv3-d16',
+    'linker_flags': [
+      '-Wl,--no-keep-memory',
+    ],
+
+  },
+
+  'target_defaults': {
+    'default_configuration': 'android-arm64_debug',
+    'configurations': {
+      'android-arm64_debug': {
+        'inherit_from': ['debug_base'],
+      },
+      'android-arm64_devel': {
+        'inherit_from': ['devel_base'],
+      },
+      'android-arm64_qa': {
+        'inherit_from': ['qa_base'],
+      },
+      'android-arm64_gold': {
+        'inherit_from': ['gold_base'],
+      },
+    }, # end of configurations
+  },
+
+  'includes': [
+    '../shared/gyp_configuration.gypi',
+  ],
+}
diff --git a/src/starboard/android/arm64/gyp_configuration.py b/src/starboard/android/arm64/gyp_configuration.py
new file mode 100644
index 0000000..215dc50
--- /dev/null
+++ b/src/starboard/android/arm64/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android ARM-64 platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+  return shared_configuration.AndroidConfiguration('android-arm64', 'arm64-v8a')
diff --git a/src/starboard/android/arm64/starboard_platform.gyp b/src/starboard/android/arm64/starboard_platform.gyp
new file mode 100644
index 0000000..5110292
--- /dev/null
+++ b/src/starboard/android/arm64/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/arm64/starboard_platform_tests.gyp b/src/starboard/android/arm64/starboard_platform_tests.gyp
new file mode 100644
index 0000000..58cd108
--- /dev/null
+++ b/src/starboard/android/arm64/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/arm64/thread_types_public.h b/src/starboard/android/arm64/thread_types_public.h
new file mode 100644
index 0000000..0b84b59
--- /dev/null
+++ b/src/starboard/android/arm64/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/android/shared/__init__.py b/src/starboard/android/shared/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/shared/__init__.py
diff --git a/src/starboard/android/shared/accessibility_get_caption_settings.cc b/src/starboard/android/shared/accessibility_get_caption_settings.cc
new file mode 100644
index 0000000..58e3067
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_caption_settings.cc
@@ -0,0 +1,322 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cmath>
+#include <cstdlib>
+#include <limits>
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/logging.h"
+
+#include "starboard/accessibility.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+const int kRgbWhite = 0xFFFFFF;
+const int kRgbBlack = 0x000000;
+const int kRgbRed = 0xFF0000;
+const int kRgbYellow = 0xFFFF00;
+const int kRgbGreen = 0x00FF00;
+const int kRgbCyan = 0x00FFFF;
+const int kRgbBlue = 0x0000FF;
+const int kRgbMagenta = 0xFF00FF;
+
+const int kRgbColors[] = {
+  kRgbWhite,
+  kRgbBlack,
+  kRgbRed,
+  kRgbYellow,
+  kRgbGreen,
+  kRgbCyan,
+  kRgbBlue,
+  kRgbMagenta,
+};
+
+SbAccessibilityCaptionColor GetClosestCaptionColor(int color) {
+  int ref_color = kRgbWhite;
+  int min_distance = std::numeric_limits<int>::max();
+
+  int r = 0xFF & (color >> 16);
+  int g = 0xFF & (color >> 8);
+  int b = 0xFF & (color);
+
+  // Find the reference color with the least distance (squared).
+  for (int i = 0; i < SB_ARRAY_SIZE(kRgbColors); i++) {
+    int r_ref = 0xFF & (kRgbColors[i] >> 16);
+    int g_ref = 0xFF & (kRgbColors[i] >> 8);
+    int b_ref = 0xFF & (kRgbColors[i]);
+    int distance_squared = pow(r - r_ref, 2) +
+                           pow(g - g_ref, 2) +
+                           pow(b - b_ref, 2);
+    if (distance_squared < min_distance) {
+      ref_color = kRgbColors[i];
+      min_distance = distance_squared;
+    }
+  }
+
+  switch (ref_color) {
+    case kRgbWhite:
+      return kSbAccessibilityCaptionColorWhite;
+    case kRgbBlack:
+      return kSbAccessibilityCaptionColorBlack;
+    case kRgbRed:
+      return kSbAccessibilityCaptionColorRed;
+    case kRgbYellow:
+      return kSbAccessibilityCaptionColorYellow;
+    case kRgbGreen:
+      return kSbAccessibilityCaptionColorGreen;
+    case kRgbCyan:
+      return kSbAccessibilityCaptionColorCyan;
+    case kRgbBlue:
+      return kSbAccessibilityCaptionColorBlue;
+    case kRgbMagenta:
+      return kSbAccessibilityCaptionColorMagenta;
+    default:
+      NOTREACHED() << "Invalid RGB color conversion";
+      return kSbAccessibilityCaptionColorWhite;
+  }
+}
+
+SbAccessibilityCaptionCharacterEdgeStyle
+AndroidEdgeTypeToSbEdgeStyle(int edge_type) {
+  switch (edge_type) {
+    case 0:
+      return kSbAccessibilityCaptionCharacterEdgeStyleNone;
+    case 1:
+      return kSbAccessibilityCaptionCharacterEdgeStyleUniform;
+    case 2:
+      return kSbAccessibilityCaptionCharacterEdgeStyleDropShadow;
+    case 3:
+      return kSbAccessibilityCaptionCharacterEdgeStyleRaised;
+    case 4:
+      return kSbAccessibilityCaptionCharacterEdgeStyleDepressed;
+    default:
+      NOTREACHED() << "Invalid edge type conversion";
+      return kSbAccessibilityCaptionCharacterEdgeStyleNone;
+  }
+}
+
+SbAccessibilityCaptionFontFamily AndroidFontFamilyToSbFontFamily(int family) {
+  switch (family) {
+    case 0:
+      return kSbAccessibilityCaptionFontFamilyCasual;
+    case 1:
+      return kSbAccessibilityCaptionFontFamilyCursive;
+    case 2:
+      return kSbAccessibilityCaptionFontFamilyMonospaceSansSerif;
+    case 3:
+      return kSbAccessibilityCaptionFontFamilyMonospaceSerif;
+    case 4:
+      return kSbAccessibilityCaptionFontFamilyProportionalSansSerif;
+    case 5:
+      return kSbAccessibilityCaptionFontFamilyProportionalSerif;
+    case 6:
+      return kSbAccessibilityCaptionFontFamilySmallCapitals;
+    default:
+      NOTREACHED() << "Invalid font family conversion";
+      return kSbAccessibilityCaptionFontFamilyCasual;
+  }
+}
+
+int FindClosestReferenceValue(int value, const int reference[],
+                              size_t reference_size) {
+  int result = reference[0];
+  int min_difference = std::numeric_limits<int>::max();
+
+  for (int i = 0; i < reference_size; i++) {
+    int difference = abs(reference[i] - value);
+    if (difference < min_difference) {
+      result = reference[i];
+      min_difference = difference;
+    }
+  }
+  return result;
+}
+
+const int kFontSizes[] = {
+  25,
+  50,
+  75,
+  100,
+  125,
+  150,
+  175,
+  200,
+  225,
+  250,
+  275,
+  300
+};
+
+SbAccessibilityCaptionFontSizePercentage GetClosestFontSizePercentage(
+    int font_size_percent) {
+  int reference_size = FindClosestReferenceValue(
+      font_size_percent, kFontSizes, SB_ARRAY_SIZE(kFontSizes));
+  switch (reference_size) {
+    case 25:
+      return kSbAccessibilityCaptionFontSizePercentage25;
+    case 50:
+      return kSbAccessibilityCaptionFontSizePercentage50;
+    case 75:
+      return kSbAccessibilityCaptionFontSizePercentage75;
+    case 100:
+      return kSbAccessibilityCaptionFontSizePercentage100;
+    case 125:
+      return kSbAccessibilityCaptionFontSizePercentage125;
+    case 150:
+      return kSbAccessibilityCaptionFontSizePercentage150;
+    case 175:
+      return kSbAccessibilityCaptionFontSizePercentage175;
+    case 200:
+      return kSbAccessibilityCaptionFontSizePercentage200;
+    case 225:
+      return kSbAccessibilityCaptionFontSizePercentage225;
+    case 250:
+      return kSbAccessibilityCaptionFontSizePercentage250;
+    case 275:
+      return kSbAccessibilityCaptionFontSizePercentage275;
+    case 300:
+      return kSbAccessibilityCaptionFontSizePercentage300;
+    default:
+      NOTREACHED() << "Invalid font size";
+      return kSbAccessibilityCaptionFontSizePercentage100;
+  }
+}
+
+const int kOpacities[] = {
+  0,
+  25,
+  50,
+  75,
+  100,
+};
+
+SbAccessibilityCaptionOpacityPercentage GetClosestOpacity(int opacity_percent) {
+  int reference_opacity_percent = FindClosestReferenceValue(
+      opacity_percent, kOpacities, SB_ARRAY_SIZE(kOpacities));
+  switch (reference_opacity_percent) {
+    case 0:
+      return kSbAccessibilityCaptionOpacityPercentage0;
+    case 25:
+      return kSbAccessibilityCaptionOpacityPercentage25;
+    case 50:
+      return kSbAccessibilityCaptionOpacityPercentage50;
+    case 75:
+      return kSbAccessibilityCaptionOpacityPercentage75;
+    case 100:
+      return kSbAccessibilityCaptionOpacityPercentage100;
+    default:
+      NOTREACHED() << "Invalid opacity percentage";
+      return kSbAccessibilityCaptionOpacityPercentage100;
+  }
+}
+
+SbAccessibilityCaptionState BooleanToCaptionState(bool is_set) {
+  if (is_set) {
+    return kSbAccessibilityCaptionStateSet;
+  } else {
+    return kSbAccessibilityCaptionStateUnset;
+  }
+}
+
+void SetColorProperties(jobject j_caption_settings,
+                        const char* color_field,
+                        const char* has_color_field,
+                        SbAccessibilityCaptionColor* color,
+                        SbAccessibilityCaptionState* color_state,
+                        SbAccessibilityCaptionOpacityPercentage* opacity,
+                        SbAccessibilityCaptionState* opacity_state) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jint j_color = env->GetIntFieldOrAbort(j_caption_settings, color_field, "I");
+  *color = GetClosestCaptionColor(j_color);
+  *opacity = GetClosestOpacity((0xFF & (j_color >> 24)) * 100 / 255);
+  *color_state = BooleanToCaptionState(
+      env->GetBooleanFieldOrAbort(j_caption_settings, has_color_field, "Z"));
+  // Color and opacity are combined into a single ARGB value.
+  // Therefore, if the color is set, so is the opacity.
+  *opacity_state = *color_state;
+}
+
+}  // namespace
+
+bool SbAccessibilityGetCaptionSettings(
+  SbAccessibilityCaptionSettings* caption_settings) {
+  if (!caption_settings ||
+      !SbMemoryIsZero(caption_settings,
+                      sizeof(SbAccessibilityCaptionSettings))) {
+    return false;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  ScopedLocalJavaRef<jobject> j_caption_settings(
+      env->CallStarboardObjectMethodOrAbort(
+          "getCaptionSettings", "()Ldev/cobalt/media/CaptionSettings;"));
+
+  jfloat font_scale =
+      env->GetFloatFieldOrAbort(j_caption_settings.Get(), "fontScale", "F");
+  caption_settings->font_size =
+      GetClosestFontSizePercentage(100.0 * font_scale);
+  // Android's captioning API always returns a font scale of 1 (100%) if
+  // the font size has not been set. This means we have no way to check if
+  // font size is "set" vs. "unset", so we'll just return "set" every time.
+  caption_settings->font_size_state = kSbAccessibilityCaptionStateSet;
+
+  // TODO: Convert Android typeface to font family.
+  caption_settings->font_family = kSbAccessibilityCaptionFontFamilyCasual;
+  caption_settings->font_family_state = kSbAccessibilityCaptionStateUnsupported;
+
+  caption_settings->character_edge_style = AndroidEdgeTypeToSbEdgeStyle(
+      env->GetIntFieldOrAbort(j_caption_settings.Get(), "edgeType", "I"));
+  caption_settings->character_edge_style_state = BooleanToCaptionState(
+      env->GetBooleanFieldOrAbort(j_caption_settings.Get(),
+                                  "hasEdgeType", "Z"));
+
+  SetColorProperties(
+      j_caption_settings.Get(), "foregroundColor", "hasForegroundColor",
+      &caption_settings->font_color,
+      &caption_settings->font_color_state,
+      &caption_settings->font_opacity,
+      &caption_settings->font_opacity_state);
+
+  SetColorProperties(
+      j_caption_settings.Get(), "backgroundColor", "hasBackgroundColor",
+      &caption_settings->background_color,
+      &caption_settings->background_color_state,
+      &caption_settings->background_opacity,
+      &caption_settings->background_opacity_state);
+
+  SetColorProperties(
+      j_caption_settings.Get(), "windowColor", "hasWindowColor",
+      &caption_settings->window_color,
+      &caption_settings->window_color_state,
+      &caption_settings->window_opacity,
+      &caption_settings->window_opacity_state);
+
+  caption_settings->is_enabled =
+     env->GetBooleanFieldOrAbort(j_caption_settings.Get(), "isEnabled", "Z");
+  caption_settings->supports_is_enabled = true;
+  caption_settings->supports_set_enabled = false;
+
+  return true;
+}
diff --git a/src/starboard/android/shared/accessibility_get_display_settings.cc b/src/starboard/android/shared/accessibility_get_display_settings.cc
new file mode 100644
index 0000000..d3d10e7
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_display_settings.cc
@@ -0,0 +1,37 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+
+bool SbAccessibilityGetDisplaySettings(
+    SbAccessibilityDisplaySettings* out_setting) {
+  if (!out_setting ||
+      !SbMemoryIsZero(out_setting,
+                      sizeof(SbAccessibilityDisplaySettings))) {
+    return false;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  out_setting->has_high_contrast_text_setting = true;
+  out_setting->is_high_contrast_text_enabled =
+      env->CallStarboardBooleanMethodOrAbort(
+          "isAccessibilityHighContrastTextEnabled", "()Z");
+
+  return true;
+}
diff --git a/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc b/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc
new file mode 100644
index 0000000..8f64452
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_get_text_to_speech_settings.cc
@@ -0,0 +1,44 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/memory.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbAccessibilityGetTextToSpeechSettings(
+    SbAccessibilityTextToSpeechSettings* out_setting) {
+  if (!out_setting ||
+      !SbMemoryIsZero(out_setting,
+                      sizeof(SbAccessibilityTextToSpeechSettings))) {
+    return false;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  out_setting->has_text_to_speech_setting = true;
+  ScopedLocalJavaRef<jobject> j_tts_helper(
+      env->CallStarboardObjectMethodOrAbort(
+          "getTextToSpeechHelper",
+          "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+  out_setting->is_text_to_speech_enabled =
+      env->CallBooleanMethodOrAbort(j_tts_helper.Get(),
+          "isScreenReaderEnabled", "()Z");
+
+  return true;
+}
diff --git a/src/starboard/android/shared/accessibility_set_captions_enabled.cc b/src/starboard/android/shared/accessibility_set_captions_enabled.cc
new file mode 100644
index 0000000..f39ad20
--- /dev/null
+++ b/src/starboard/android/shared/accessibility_set_captions_enabled.cc
@@ -0,0 +1,21 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/accessibility.h"
+#include "starboard/configuration.h"
+
+bool SbAccessibilitySetCaptionsEnabled(bool enabled) {
+  SB_UNREFERENCED_PARAMETER(enabled);
+  return false;
+}
diff --git a/src/starboard/android/shared/android_main.cc b/src/starboard/android/shared/android_main.cc
new file mode 100644
index 0000000..771ad25
--- /dev/null
+++ b/src/starboard/android/shared/android_main.cc
@@ -0,0 +1,214 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/log_internal.h"
+#include "starboard/common/semaphore.h"
+#include "starboard/shared/starboard/command_line.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+using ::starboard::shared::starboard::CommandLine;
+typedef ::starboard::android::shared::ApplicationAndroid::AndroidCommand
+    AndroidCommand;
+
+SbThread g_starboard_thread = kSbThreadInvalid;
+
+// Safeguard to avoid sending AndroidCommands either when there is no instance
+// of the Starboard application, or after the run loop has exited and the
+// ALooper receiving the commands is no longer being polled.
+bool g_app_running = false;
+
+std::vector<std::string> GetArgs() {
+  std::vector<std::string> args;
+  // Fake program name as args[0]
+  args.push_back(SbStringDuplicate("android_main"));
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  ScopedLocalJavaRef<jobjectArray> args_array(
+      env->CallStarboardObjectMethodOrAbort("getArgs",
+                                            "()[Ljava/lang/String;"));
+  jint argc = !args_array ? 0 : env->GetArrayLength(args_array.Get());
+
+  for (jint i = 0; i < argc; i++) {
+    ScopedLocalJavaRef<jstring> element(
+        env->GetObjectArrayElementOrAbort(args_array.Get(), i));
+    args.push_back(env->GetStringStandardUTFOrAbort(element.Get()));
+  }
+
+  return args;
+}
+
+std::string GetStartDeepLink() {
+  JniEnvExt* env = JniEnvExt::Get();
+  std::string start_url;
+
+  ScopedLocalJavaRef<jstring> j_url(env->CallStarboardObjectMethodOrAbort(
+      "getStartDeepLink", "()Ljava/lang/String;"));
+  if (j_url) {
+    start_url = env->GetStringStandardUTFOrAbort(j_url.Get());
+  }
+  return start_url;
+}
+
+void* ThreadEntryPoint(void* context) {
+  Semaphore* app_created_semaphore = static_cast<Semaphore*>(context);
+
+  ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
+  ApplicationAndroid app(looper);
+
+  CommandLine command_line(GetArgs());
+  LogInit(command_line);
+
+  // Mark the app running before signaling app created so there's no race to
+  // allow sending the first AndroidCommand after onCreate() returns.
+  g_app_running = true;
+
+  // Signal ANativeActivity_onCreate() that it may proceed.
+  app_created_semaphore->Put();
+
+  // Enter the Starboard run loop until stopped.
+  int error_level =
+      app.Run(std::move(command_line), GetStartDeepLink().c_str());
+
+  // Mark the app not running before informing StarboardBridge that the app is
+  // stopped so that we won't send any more AndroidCommands as a result of
+  // shutting down the Activity.
+  g_app_running = false;
+
+  // Our launcher.py looks for this to know when the app (test) is done.
+  SB_LOG(INFO) << "***Application Stopped*** " << error_level;
+
+  // Inform StarboardBridge that the run loop has exited so it can cleanup and
+  // kill the process.
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallStarboardVoidMethodOrAbort("afterStopped", "()V");
+
+  return NULL;
+}
+
+void OnStart(ANativeActivity* activity) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStart);
+  }
+}
+
+void OnResume(ANativeActivity* activity) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kResume);
+  }
+}
+
+void OnPause(ANativeActivity* activity) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kPause);
+  }
+}
+
+void OnStop(ANativeActivity* activity) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStop);
+  }
+}
+
+void OnWindowFocusChanged(ANativeActivity* activity, int focused) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(focused
+        ? AndroidCommand::kWindowFocusGained
+        : AndroidCommand::kWindowFocusLost);
+  }
+}
+
+void OnNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(
+        AndroidCommand::kNativeWindowCreated, window);
+  }
+}
+
+void OnNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(
+        AndroidCommand::kNativeWindowDestroyed);
+  }
+}
+
+void OnInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(
+        AndroidCommand::kInputQueueChanged, queue);
+  }
+}
+
+void OnInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
+  if (g_app_running) {
+    ApplicationAndroid::Get()->SendAndroidCommand(
+        AndroidCommand::kInputQueueChanged, NULL);
+  }
+}
+
+extern "C" SB_EXPORT_PLATFORM void ANativeActivity_onCreate(
+    ANativeActivity *activity, void *savedState, size_t savedStateSize) {
+
+  // Start the Starboard thread the first time an Activity is created.
+  if (!SbThreadIsValid(g_starboard_thread)) {
+    Semaphore semaphore;
+
+    g_starboard_thread = SbThreadCreate(
+        0, kSbThreadPriorityNormal, kSbThreadNoAffinity, false,
+        "StarboardMain", &ThreadEntryPoint, &semaphore);
+
+    // Wait for the ApplicationAndroid to be created.
+    semaphore.Take();
+  }
+
+  activity->callbacks->onStart = OnStart;
+  activity->callbacks->onResume = OnResume;
+  activity->callbacks->onPause = OnPause;
+  activity->callbacks->onStop = OnStop;
+  activity->callbacks->onWindowFocusChanged = OnWindowFocusChanged;
+  activity->callbacks->onNativeWindowCreated = OnNativeWindowCreated;
+  activity->callbacks->onNativeWindowDestroyed = OnNativeWindowDestroyed;
+  activity->callbacks->onInputQueueCreated = OnInputQueueCreated;
+  activity->callbacks->onInputQueueDestroyed = OnInputQueueDestroyed;
+  activity->instance = ApplicationAndroid::Get();
+}
+
+extern "C" SB_EXPORT_PLATFORM
+jboolean Java_dev_cobalt_coat_StarboardBridge_nativeIsReleaseBuild() {
+#if defined(COBALT_BUILD_TYPE_GOLD)
+  return true;
+#else
+  return false;
+#endif
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeInitialize(
+    JniEnvExt* env, jobject starboard_bridge) {
+  JniEnvExt::Initialize(env, starboard_bridge);
+}
+
+}  // namespace
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/application_android.cc b/src/starboard/android/shared/application_android.cc
new file mode 100644
index 0000000..5420592
--- /dev/null
+++ b/src/starboard/android/shared/application_android.cc
@@ -0,0 +1,442 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/application_android.h"
+
+#include <android/looper.h>
+#include <android/native_activity.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "starboard/accessibility.h"
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/android/shared/input_events_generator.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/condition_variable.h"
+#include "starboard/event.h"
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/audio_sink/audio_sink_internal.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+  enum {
+    kLooperIdAndroidCommand,
+    kLooperIdAndroidInput,
+    kLooperIdKeyboardInject,
+  };
+
+  const char* AndroidCommandName(
+      ApplicationAndroid::AndroidCommand::CommandType type) {
+    switch (type) {
+      case ApplicationAndroid::AndroidCommand::kUndefined:
+        return "Undefined";
+      case ApplicationAndroid::AndroidCommand::kStart:
+        return "Start";
+      case ApplicationAndroid::AndroidCommand::kResume:
+        return "Resume";
+      case ApplicationAndroid::AndroidCommand::kPause:
+        return "Pause";
+      case ApplicationAndroid::AndroidCommand::kStop:
+        return "Stop";
+      case ApplicationAndroid::AndroidCommand::kInputQueueChanged:
+        return "InputQueueChanged";
+      case ApplicationAndroid::AndroidCommand::kNativeWindowCreated:
+        return "NativeWindowCreated";
+      case ApplicationAndroid::AndroidCommand::kNativeWindowDestroyed:
+        return "NativeWindowDestroyed";
+      case ApplicationAndroid::AndroidCommand::kWindowFocusGained:
+        return "WindowFocusGained";
+      case ApplicationAndroid::AndroidCommand::kWindowFocusLost:
+        return "WindowFocusLost";
+      default:
+        return "unknown";
+    }
+  }
+}  // namespace
+
+// "using" doesn't work with class members, so make a local convenience type.
+typedef ::starboard::shared::starboard::Application::Event Event;
+
+ApplicationAndroid::ApplicationAndroid(ALooper* looper)
+    : looper_(looper),
+      native_window_(NULL),
+      input_queue_(NULL),
+      android_command_readfd_(-1),
+      android_command_writefd_(-1),
+      keyboard_inject_readfd_(-1),
+      keyboard_inject_writefd_(-1),
+      android_command_condition_(android_command_mutex_),
+      activity_state_(AndroidCommand::kUndefined),
+      window_(kSbWindowInvalid),
+      last_is_accessibility_high_contrast_text_enabled_(false) {
+
+  // Initialize Time Zone early so that local time works correctly.
+  // Called once here to help SbTimeZoneGet*Name()
+  tzset();
+
+  // Initialize Android asset access early so that ICU can load its tables
+  // from the assets. The use ICU is used in our logging.
+  SbFileAndroidInitialize();
+
+  int pipefd[2];
+  int err;
+
+  err = pipe(pipefd);
+  SB_CHECK(err >= 0) << "pipe errno is:" << errno;
+  android_command_readfd_ = pipefd[0];
+  android_command_writefd_ = pipefd[1];
+  ALooper_addFd(looper_, android_command_readfd_, kLooperIdAndroidCommand,
+                ALOOPER_EVENT_INPUT, NULL, NULL);
+
+  err = pipe(pipefd);
+  SB_CHECK(err >= 0) << "pipe errno is:" << errno;
+  keyboard_inject_readfd_ = pipefd[0];
+  keyboard_inject_writefd_ = pipefd[1];
+  ALooper_addFd(looper_, keyboard_inject_readfd_, kLooperIdKeyboardInject,
+                ALOOPER_EVENT_INPUT, NULL, NULL);
+}
+
+ApplicationAndroid::~ApplicationAndroid() {
+  ALooper_removeFd(looper_, android_command_readfd_);
+  close(android_command_readfd_);
+  close(android_command_writefd_);
+
+  ALooper_removeFd(looper_, keyboard_inject_readfd_);
+  close(keyboard_inject_readfd_);
+  close(keyboard_inject_writefd_);
+}
+
+void ApplicationAndroid::Initialize() {
+  SbAudioSinkPrivate::Initialize();
+}
+
+void ApplicationAndroid::Teardown() {
+  SbAudioSinkPrivate::TearDown();
+  SbFileAndroidTeardown();
+}
+
+SbWindow ApplicationAndroid::CreateWindow(const SbWindowOptions* options) {
+  SB_UNREFERENCED_PARAMETER(options);
+  if (SbWindowIsValid(window_)) {
+    return kSbWindowInvalid;
+  }
+  window_ = new SbWindowPrivate;
+  window_->native_window = native_window_;
+  input_events_generator_.reset(new InputEventsGenerator(window_));
+  return window_;
+}
+
+bool ApplicationAndroid::DestroyWindow(SbWindow window) {
+  if (!SbWindowIsValid(window)) {
+    return false;
+  }
+
+  input_events_generator_.reset();
+
+  SB_DCHECK(window == window_);
+  delete window_;
+  window_ = kSbWindowInvalid;
+  return true;
+}
+
+Event* ApplicationAndroid::WaitForSystemEventWithTimeout(SbTime time) {
+  // Convert from microseconds to milliseconds, taking the ceiling value.
+  // If we take the floor, or round, then we end up busy looping every time
+  // the next event time is less than one millisecond.
+  int timeout_millis = (time + kSbTimeMillisecond - 1) / kSbTimeMillisecond;
+  int looper_events;
+  int ident = ALooper_pollAll(timeout_millis, NULL, &looper_events, NULL);
+  switch (ident) {
+    case kLooperIdAndroidCommand:
+      ProcessAndroidCommand();
+      break;
+    case kLooperIdAndroidInput:
+      ProcessAndroidInput();
+      break;
+    case kLooperIdKeyboardInject:
+      ProcessKeyboardInject();
+      break;
+  }
+
+  // Always return NULL since we already dispatched our own system events.
+  return NULL;
+}
+
+void ApplicationAndroid::WakeSystemEventWait() {
+  ALooper_wake(looper_);
+}
+
+void ApplicationAndroid::OnResume() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallStarboardVoidMethodOrAbort("beforeStartOrResume", "()V");
+}
+
+void ApplicationAndroid::ProcessAndroidCommand() {
+  JniEnvExt* env = JniEnvExt::Get();
+  AndroidCommand cmd;
+  int err = read(android_command_readfd_, &cmd, sizeof(cmd));
+  SB_DCHECK(err >= 0) << "Command read failed. errno=" << errno;
+
+  SB_LOG(INFO) << "Android command: " << AndroidCommandName(cmd.type);
+
+  // The activity state to which we should sync the starboard state.
+  AndroidCommand::CommandType sync_state = AndroidCommand::kUndefined;
+
+  switch (cmd.type) {
+    case AndroidCommand::kUndefined:
+      break;
+
+    case AndroidCommand::kInputQueueChanged: {
+      ScopedLock lock(android_command_mutex_);
+      if (input_queue_) {
+        AInputQueue_detachLooper(input_queue_);
+      }
+      input_queue_ = static_cast<AInputQueue*>(cmd.data);
+      if (input_queue_) {
+        AInputQueue_attachLooper(input_queue_, looper_, kLooperIdAndroidInput,
+                                 NULL, NULL);
+      }
+      // Now that we've swapped our use of the input queue, signal that the
+      // Android UI thread can continue.
+      android_command_condition_.Signal();
+      break;
+    }
+
+    // Starboard resume/suspend is tied to the UI window being created/destroyed
+    // (rather than to the Activity lifecycle) since Cobalt can't do anything at
+    // all if it doesn't have a window surface to draw on.
+    case AndroidCommand::kNativeWindowCreated:
+      {
+        ScopedLock lock(android_command_mutex_);
+        native_window_ = static_cast<ANativeWindow*>(cmd.data);
+        if (window_) {
+          window_->native_window = native_window_;
+        }
+        // Now that we have the window, signal that the Android UI thread can
+        // continue, before we start or resume the Starboard app.
+        android_command_condition_.Signal();
+      }
+      if (state() == kStateUnstarted) {
+        // This is the initial launch, so we have to start Cobalt now that we
+        // have a window.
+        env->CallStarboardVoidMethodOrAbort("beforeStartOrResume", "()V");
+        DispatchStart();
+      } else {
+        // Now that we got a window back, change the command for the switch
+        // below to sync up with the current activity lifecycle.
+        sync_state = activity_state_;
+      }
+      break;
+    case AndroidCommand::kNativeWindowDestroyed:
+      env->CallStarboardVoidMethodOrAbort("beforeSuspend", "()V");
+      {
+        ScopedLock lock(android_command_mutex_);
+        // Cobalt can't keep running without a window, even if the Activity
+        // hasn't stopped yet. DispatchAndDelete() will inject events as needed
+        // if we're not already paused.
+        DispatchAndDelete(new Event(kSbEventTypeSuspend, NULL, NULL));
+        if (window_) {
+          window_->native_window = NULL;
+        }
+        native_window_ = NULL;
+        // Now that we've suspended the Starboard app, and let go of the window,
+        // signal that the Android UI thread can continue.
+        android_command_condition_.Signal();
+      }
+      break;
+
+    case AndroidCommand::kWindowFocusLost:
+      break;
+    case AndroidCommand::kWindowFocusGained: {
+      // Android does not have a publicly-exposed way to
+      // register for high-contrast text settings changed events.
+      // We assume that it can only change when our focus changes
+      // (because the user exits and enters the app) so we check
+      // for changes here.
+      SbAccessibilityDisplaySettings settings;
+      SbMemorySet(&settings, 0, sizeof(settings));
+      if (!SbAccessibilityGetDisplaySettings(&settings)) {
+        break;
+      }
+
+      bool enabled = settings.has_high_contrast_text_setting &&
+          settings.is_high_contrast_text_enabled;
+
+      if (enabled != last_is_accessibility_high_contrast_text_enabled_) {
+        DispatchAndDelete(new Event(
+            kSbEventTypeAccessiblitySettingsChanged, NULL, NULL));
+      }
+      last_is_accessibility_high_contrast_text_enabled_ = enabled;
+      break;
+    }
+
+    // Remember the Android activity state to sync to when we have a window.
+    case AndroidCommand::kStart:
+    case AndroidCommand::kResume:
+    case AndroidCommand::kPause:
+    case AndroidCommand::kStop:
+      sync_state = activity_state_ = cmd.type;
+      break;
+  }
+
+  // If there's a window, sync the app state to the Activity lifecycle, letting
+  // DispatchAndDelete() inject events as needed if we missed a state.
+  if (native_window_) {
+    switch (sync_state) {
+      case AndroidCommand::kStart:
+        DispatchAndDelete(new Event(kSbEventTypeResume, NULL, NULL));
+        break;
+      case AndroidCommand::kResume:
+        DispatchAndDelete(new Event(kSbEventTypeUnpause, NULL, NULL));
+        break;
+      case AndroidCommand::kPause:
+        DispatchAndDelete(new Event(kSbEventTypePause, NULL, NULL));
+        break;
+      case AndroidCommand::kStop:
+        if (state() != kStateSuspended) {
+          // We usually suspend when losing the window above, but if the window
+          // wasn't destroyed (e.g. when Daydream starts) then we still have to
+          // suspend when the Activity is stopped.
+          env->CallStarboardVoidMethodOrAbort("beforeSuspend", "()V");
+          DispatchAndDelete(new Event(kSbEventTypeSuspend, NULL, NULL));
+        }
+        break;
+      default:
+        break;
+    }
+  }
+}
+
+void ApplicationAndroid::SendAndroidCommand(AndroidCommand::CommandType type,
+                                            void* data) {
+  SB_LOG(INFO) << "Send Android command: " << AndroidCommandName(type);
+  AndroidCommand cmd {type, data};
+  ScopedLock lock(android_command_mutex_);
+  write(android_command_writefd_, &cmd, sizeof(cmd));
+  // Synchronization only necessary when managing resources.
+  switch (type) {
+    case AndroidCommand::kInputQueueChanged:
+      while (input_queue_ != data) {
+        android_command_condition_.Wait();
+      }
+      break;
+    case AndroidCommand::kNativeWindowCreated:
+    case AndroidCommand::kNativeWindowDestroyed:
+      while (native_window_ != data) {
+        android_command_condition_.Wait();
+      }
+      break;
+    default:
+      break;
+  }
+}
+
+void ApplicationAndroid::ProcessAndroidInput() {
+  SB_DCHECK(input_events_generator_);
+  AInputEvent* android_event = NULL;
+  while (AInputQueue_getEvent(input_queue_, &android_event) >= 0) {
+    SB_LOG(INFO) << "Android input: type="
+                 << AInputEvent_getType(android_event);
+    if (AInputQueue_preDispatchEvent(input_queue_, android_event)) {
+        continue;
+    }
+    InputEventsGenerator::Events app_events;
+    bool handled = input_events_generator_->CreateInputEventsFromAndroidEvent(
+        android_event, &app_events);
+    for (int i = 0; i < app_events.size(); ++i) {
+      DispatchAndDelete(app_events[i].release());
+    }
+    AInputQueue_finishEvent(input_queue_, android_event, handled);
+  }
+}
+
+void ApplicationAndroid::ProcessKeyboardInject() {
+  SbKey key;
+  int err = read(keyboard_inject_readfd_, &key, sizeof(key));
+  SB_DCHECK(err >= 0) << "Keyboard inject read failed: errno=" << errno;
+  SB_LOG(INFO) << "Keyboard inject: " << key;
+
+  InputEventsGenerator::Events app_events;
+  input_events_generator_->CreateInputEventsFromSbKey(key, &app_events);
+  for (int i = 0; i < app_events.size(); ++i) {
+    DispatchAndDelete(app_events[i].release());
+  }
+}
+
+void ApplicationAndroid::SendKeyboardInject(SbKey key) {
+  write(keyboard_inject_writefd_, &key, sizeof(key));
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_CobaltA11yHelper_nativeInjectKeyEvent(JNIEnv* env,
+                                                           jobject unused_clazz,
+                                                           jint key) {
+  ApplicationAndroid::Get()->SendKeyboardInject(static_cast<SbKey>(key));
+}
+
+bool ApplicationAndroid::OnSearchRequested() {
+  for (int i = 0; i < 2; i++) {
+    SbInputData* data = new SbInputData();
+    SbMemorySet(data, 0, sizeof(*data));
+    data->window = window_;
+    data->key = kSbKeyBrowserSearch;
+    data->type = (i == 0) ? kSbInputEventTypePress : kSbInputEventTypeUnpress;
+    Inject(new Event(kSbEventTypeInput, data, &DeleteDestructor<SbInputData>));
+  }
+  return true;
+}
+
+extern "C" SB_EXPORT_PLATFORM
+jboolean Java_dev_cobalt_coat_StarboardBridge_nativeOnSearchRequested(
+    JniEnvExt* env, jobject unused_this) {
+  return ApplicationAndroid::Get()->OnSearchRequested();
+}
+
+void ApplicationAndroid::HandleDeepLink(const char* link_url) {
+  if (link_url == NULL || link_url[0] == '\0') {
+    return;
+  }
+  char* deep_link = SbStringDuplicate(link_url);
+  SB_DCHECK(deep_link);
+  Inject(new Event(kSbEventTypeLink, deep_link, SbMemoryDeallocate));
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeHandleDeepLink(
+    JniEnvExt* env, jobject unused_this, jstring j_url) {
+  if (j_url) {
+    std::string utf_str = env->GetStringStandardUTFOrAbort(j_url);
+    ApplicationAndroid::Get()->HandleDeepLink(utf_str.c_str());
+  }
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_StarboardBridge_nativeStopApp(
+    JniEnvExt* env, jobject unused_this, jint error_level) {
+  ApplicationAndroid::Get()->Stop(error_level);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/application_android.h b/src/starboard/android/shared/application_android.h
new file mode 100644
index 0000000..011ddcf
--- /dev/null
+++ b/src/starboard/android/shared/application_android.h
@@ -0,0 +1,124 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
+#define STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
+
+#include <android/looper.h>
+#include <android/native_window.h>
+
+#include "starboard/android/shared/input_events_generator.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/condition_variable.h"
+#include "starboard/configuration.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/application.h"
+#include "starboard/shared/starboard/queue_application.h"
+#include "starboard/types.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Android application receiving commands and input through ALooper.
+class ApplicationAndroid
+    : public ::starboard::shared::starboard::QueueApplication {
+ public:
+  struct AndroidCommand {
+    typedef enum {
+      kUndefined,
+      kStart,
+      kResume,
+      kPause,
+      kStop,
+      kInputQueueChanged,
+      kNativeWindowCreated,
+      kNativeWindowDestroyed,
+      kWindowFocusGained,
+      kWindowFocusLost,
+    } CommandType;
+
+    CommandType type;
+    void* data;
+  };
+
+  explicit ApplicationAndroid(ALooper* looper);
+  ~ApplicationAndroid() override;
+
+  static ApplicationAndroid* Get() {
+    return static_cast<ApplicationAndroid*>(
+        ::starboard::shared::starboard::Application::Get());
+  }
+
+  SbWindow CreateWindow(const SbWindowOptions* options);
+  bool DestroyWindow(SbWindow window);
+  bool OnSearchRequested();
+  void HandleDeepLink(const char* link_url);
+
+  void SendAndroidCommand(AndroidCommand::CommandType type, void* data);
+  void SendAndroidCommand(AndroidCommand::CommandType type) {
+    SendAndroidCommand(type, NULL);
+  }
+  void SendKeyboardInject(SbKey key);
+
+ protected:
+  // --- Application overrides ---
+  void Initialize() override;
+  void Teardown() override;
+  bool IsStartImmediate() override { return false; }
+  void OnResume() override;
+
+  // --- QueueApplication overrides ---
+  bool MayHaveSystemEvents() override { return true; }
+  Event* WaitForSystemEventWithTimeout(SbTime time) override;
+  void WakeSystemEventWait() override;
+
+ private:
+  ALooper* looper_;
+  ANativeWindow* native_window_;
+  AInputQueue* input_queue_;
+
+  // Pipes attached to the looper.
+  int android_command_readfd_;
+  int android_command_writefd_;
+  int keyboard_inject_readfd_;
+  int keyboard_inject_writefd_;
+
+  // Synchronization for commands that change availability of Android resources
+  // such as the input_queue_ and/or native_window_.
+  Mutex android_command_mutex_;
+  ConditionVariable android_command_condition_;
+
+  // The last Activity lifecycle state command received.
+  AndroidCommand::CommandType activity_state_;
+
+  // The single open window, if any.
+  SbWindow window_;
+
+  scoped_ptr<InputEventsGenerator> input_events_generator_;
+
+  bool last_is_accessibility_high_contrast_text_enabled_;
+
+  // Methods to process pipes attached to the Looper.
+  void ProcessAndroidCommand();
+  void ProcessAndroidInput();
+  void ProcessKeyboardInject();
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_APPLICATION_ANDROID_H_
diff --git a/src/starboard/android/shared/atomic_public.h b/src/starboard/android/shared/atomic_public.h
new file mode 100644
index 0000000..5bd90ca
--- /dev/null
+++ b/src/starboard/android/shared/atomic_public.h
@@ -0,0 +1,26 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
+
+#include "starboard/atomic.h"
+
+#if SB_IS(COMPILER_GCC)
+#include "starboard/shared/gcc/atomic_gcc_public.h"
+#else
+#error "Unknown Android compiler."
+#endif
+
+#endif  // STARBOARD_ANDROID_SHARED_ATOMIC_PUBLIC_H_
diff --git a/src/starboard/android/shared/audio_decoder.cc b/src/starboard/android/shared/audio_decoder.cc
new file mode 100644
index 0000000..5bd0905
--- /dev/null
+++ b/src/starboard/android/shared/audio_decoder.cc
@@ -0,0 +1,250 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/audio_decoder.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/audio_sink.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+// Can be locally set to |1| for verbose audio decoding.  Verbose audio
+// decoding will log the following transitions that take place for each audio
+// unit:
+//   T1: Our client passes an |InputBuffer| of audio data into us.
+//   T2: We receive a corresponding media codec output buffer back from our
+//       |MediaCodecBridge|.
+//   T3: Our client reads a corresponding |DecodedAudio| out of us.
+//
+// Example usage for debugging audio playback:
+//   $ adb logcat -c
+//   $ adb logcat | tee log.txt
+//   # Play video and get to frozen point.
+//   $ CTRL-C
+//   $ cat log.txt | grep -P 'T2: pts \d+' | wc -l
+//   523
+//   $ cat log.txt | grep -P 'T3: pts \d+' | wc -l
+//   522
+//   # Oh no, why isn't our client reading the audio we have ready to go?
+//   # Time to go find out...
+#define STARBOARD_ANDROID_SHARED_AUDIO_DECODER_VERBOSE 0
+#if STARBOARD_ANDROID_SHARED_AUDIO_DECODER_VERBOSE
+#define VERBOSE_MEDIA_LOG() SB_LOG(INFO)
+#else
+#define VERBOSE_MEDIA_LOG() SB_EAT_STREAM_PARAMETERS
+#endif
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+SbMediaAudioSampleType GetSupportedSampleType() {
+  SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(
+      kSbMediaAudioSampleTypeInt16Deprecated));
+  return kSbMediaAudioSampleTypeInt16Deprecated;
+}
+
+void* IncrementPointerByBytes(void* pointer, int offset) {
+  return static_cast<uint8_t*>(pointer) + offset;
+}
+
+}  // namespace
+
+AudioDecoder::AudioDecoder(SbMediaAudioCodec audio_codec,
+                           const SbMediaAudioHeader& audio_header,
+                           SbDrmSystem drm_system)
+    : audio_codec_(audio_codec),
+      audio_header_(audio_header),
+      sample_type_(GetSupportedSampleType()),
+      output_sample_rate_(audio_header.samples_per_second),
+      output_channel_count_(audio_header.number_of_channels),
+      drm_system_(static_cast<DrmSystem*>(drm_system)) {
+  if (!InitializeCodec()) {
+    SB_LOG(ERROR) << "Failed to initialize audio decoder.";
+  }
+}
+
+AudioDecoder::~AudioDecoder() {}
+
+void AudioDecoder::Initialize(const OutputCB& output_cb,
+                              const ErrorCB& error_cb) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb);
+  SB_DCHECK(!output_cb_);
+  SB_DCHECK(error_cb);
+  SB_DCHECK(!error_cb_);
+  SB_DCHECK(media_decoder_);
+
+  output_cb_ = output_cb;
+  error_cb_ = error_cb;
+
+  media_decoder_->Initialize(error_cb_);
+}
+
+void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+                          const ConsumedCB& consumed_cb) {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(input_buffer);
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(media_decoder_);
+
+  VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+
+  media_decoder_->WriteInputBuffer(input_buffer);
+
+  ScopedLock lock(decoded_audios_mutex_);
+  if (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <=
+      kMaxPendingWorkSize) {
+    Schedule(consumed_cb);
+  } else {
+    consumed_cb_ = consumed_cb;
+  }
+}
+
+void AudioDecoder::WriteEndOfStream() {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(media_decoder_);
+
+  media_decoder_->WriteEndOfStream();
+}
+
+scoped_refptr<AudioDecoder::DecodedAudio> AudioDecoder::Read() {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb_);
+
+  scoped_refptr<DecodedAudio> result;
+  {
+    starboard::ScopedLock lock(decoded_audios_mutex_);
+    SB_DCHECK(!decoded_audios_.empty());
+    if (!decoded_audios_.empty()) {
+      result = decoded_audios_.front();
+      VERBOSE_MEDIA_LOG() << "T3: timestamp " << result->timestamp();
+      decoded_audios_.pop();
+    }
+  }
+
+  if (consumed_cb_) {
+    Schedule(consumed_cb_);
+    consumed_cb_ = nullptr;
+  }
+  return result;
+}
+
+void AudioDecoder::Reset() {
+  SB_DCHECK(BelongsToCurrentThread());
+  SB_DCHECK(output_cb_);
+
+  media_decoder_.reset();
+
+  if (!InitializeCodec()) {
+    // TODO: Communicate this failure to our clients somehow.
+    SB_LOG(ERROR) << "Failed to initialize codec after reset.";
+  }
+
+  consumed_cb_ = nullptr;
+
+  while (!decoded_audios_.empty()) {
+    decoded_audios_.pop();
+  }
+
+  CancelPendingJobs();
+}
+
+bool AudioDecoder::InitializeCodec() {
+  SB_DCHECK(!media_decoder_);
+  media_decoder_.reset(
+      new MediaDecoder(this, audio_codec_, audio_header_, drm_system_));
+  if (media_decoder_->is_valid()) {
+    if (error_cb_) {
+      media_decoder_->Initialize(error_cb_);
+    }
+    return true;
+  }
+  media_decoder_.reset();
+  return false;
+}
+
+void AudioDecoder::ProcessOutputBuffer(
+    MediaCodecBridge* media_codec_bridge,
+    const DequeueOutputResult& dequeue_output_result) {
+  SB_DCHECK(media_codec_bridge);
+  SB_DCHECK(output_cb_);
+  SB_DCHECK(dequeue_output_result.index >= 0);
+
+  if (dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM) {
+    media_codec_bridge->ReleaseOutputBuffer(dequeue_output_result.index, false);
+    {
+      starboard::ScopedLock lock(decoded_audios_mutex_);
+      decoded_audios_.push(new DecodedAudio());
+    }
+
+    Schedule(output_cb_);
+    return;
+  }
+
+  ScopedJavaByteBuffer byte_buffer(
+      media_codec_bridge->GetOutputBuffer(dequeue_output_result.index));
+  SB_DCHECK(!byte_buffer.IsNull());
+
+  if (dequeue_output_result.num_bytes > 0) {
+    int16_t* data = static_cast<int16_t*>(IncrementPointerByBytes(
+        byte_buffer.address(), dequeue_output_result.offset));
+    int size = dequeue_output_result.num_bytes;
+    if (2 * audio_header_.samples_per_second == output_sample_rate_) {
+      // The audio is encoded using implicit HE-AAC.  As the audio sink has
+      // been created already we try to down-mix the decoded data to half of
+      // its channels so the audio sink can play it with the correct pitch.
+      for (int i = 0; i < size / sizeof(int16_t); i++) {
+        data[i / 2] = (static_cast<int32_t>(data[i]) +
+                       static_cast<int32_t>(data[i + 1]) / 2);
+      }
+      size /= 2;
+    }
+
+    scoped_refptr<DecodedAudio> decoded_audio = new DecodedAudio(
+        audio_header_.number_of_channels, GetSampleType(), GetStorageType(),
+        dequeue_output_result.presentation_time_microseconds, size);
+
+    SbMemoryCopy(decoded_audio->buffer(), data, size);
+    {
+      starboard::ScopedLock lock(decoded_audios_mutex_);
+      decoded_audios_.push(decoded_audio);
+      VERBOSE_MEDIA_LOG() << "T2: timestamp "
+                          << decoded_audios_.front()->timestamp();
+    }
+    Schedule(output_cb_);
+  }
+
+  media_codec_bridge->ReleaseOutputBuffer(dequeue_output_result.index, false);
+}
+
+void AudioDecoder::RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) {
+  AudioOutputFormatResult output_format =
+      media_codec_bridge->GetAudioOutputFormat();
+  if (output_format.status == MEDIA_CODEC_ERROR) {
+    SB_LOG(ERROR) << "|getOutputFormat| failed";
+    return;
+  }
+  output_sample_rate_ = output_format.sample_rate;
+  output_channel_count_ = output_format.channel_count;
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/audio_decoder.h b/src/starboard/android/shared/audio_decoder.h
new file mode 100644
index 0000000..c18e4a4
--- /dev/null
+++ b/src/starboard/android/shared/audio_decoder.h
@@ -0,0 +1,100 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
+
+#include <jni.h>
+
+#include <queue>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/android/shared/media_decoder.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/decoded_audio_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/job_queue.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class AudioDecoder
+    : public ::starboard::shared::starboard::player::filter::AudioDecoder,
+      private ::starboard::shared::starboard::player::JobQueue::JobOwner,
+      private MediaDecoder::Host {
+ public:
+  AudioDecoder(SbMediaAudioCodec audio_codec,
+               const SbMediaAudioHeader& audio_header,
+               SbDrmSystem drm_system);
+  ~AudioDecoder() override;
+
+  void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
+  void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+              const ConsumedCB& consumed_cb) override;
+  void WriteEndOfStream() override;
+  scoped_refptr<DecodedAudio> Read() override;
+  void Reset() override;
+
+  SbMediaAudioSampleType GetSampleType() const override {
+    return sample_type_;
+  }
+  SbMediaAudioFrameStorageType GetStorageType() const override {
+    return kSbMediaAudioFrameStorageTypeInterleaved;
+  }
+  int GetSamplesPerSecond() const override {
+    return audio_header_.samples_per_second;
+  }
+
+  bool is_valid() const { return media_decoder_ != NULL; }
+
+ private:
+  // The maximum amount of work that can exist in the union of |EventQueue|,
+  // |pending_work| and |decoded_audios_|.
+  static const int kMaxPendingWorkSize = 64;
+
+  bool InitializeCodec();
+  void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+                           const DequeueOutputResult& output) override;
+  void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) override;
+  bool Tick(MediaCodecBridge* media_codec_bridge) override { return false; }
+  void OnFlushing() override {}
+
+  SbMediaAudioCodec audio_codec_;
+  SbMediaAudioHeader audio_header_;
+  SbMediaAudioSampleType sample_type_;
+
+  jint output_sample_rate_;
+  jint output_channel_count_;
+
+  DrmSystem* drm_system_;
+
+  OutputCB output_cb_;
+  ErrorCB error_cb_;
+  ConsumedCB consumed_cb_;
+
+  starboard::Mutex decoded_audios_mutex_;
+  std::queue<scoped_refptr<DecodedAudio> > decoded_audios_;
+
+  scoped_ptr<MediaDecoder> media_decoder_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_AUDIO_DECODER_H_
diff --git a/src/starboard/android/shared/audio_sink_get_max_channels.cc b/src/starboard/android/shared/audio_sink_get_max_channels.cc
new file mode 100644
index 0000000..979f62f
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_get_max_channels.cc
@@ -0,0 +1,19 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+int SbAudioSinkGetMaxChannels() {
+  return 6;
+}
diff --git a/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc b/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc
new file mode 100644
index 0000000..bc753b9
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_get_nearest_supported_sample_frequency.cc
@@ -0,0 +1,27 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+#include "starboard/log.h"
+
+int SbAudioSinkGetNearestSupportedSampleFrequency(int sampling_frequency_hz) {
+  if (sampling_frequency_hz <= 0) {
+    SB_LOG(ERROR) << "Invalid audio sampling frequency "
+                  << sampling_frequency_hz;
+    return 1;
+  }
+
+  return sampling_frequency_hz;
+}
diff --git a/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc b/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc
new file mode 100644
index 0000000..771410e
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_is_audio_frame_storage_type_supported.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+bool SbAudioSinkIsAudioFrameStorageTypeSupported(
+    SbMediaAudioFrameStorageType audio_frame_storage_type) {
+  return audio_frame_storage_type == kSbMediaAudioFrameStorageTypeInterleaved;
+}
diff --git a/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc b/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc
new file mode 100644
index 0000000..1de6614
--- /dev/null
+++ b/src/starboard/android/shared/audio_sink_is_audio_sample_type_supported.cc
@@ -0,0 +1,21 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/audio_sink.h"
+
+bool SbAudioSinkIsAudioSampleTypeSupported(
+    SbMediaAudioSampleType audio_sample_type) {
+  return audio_sample_type == kSbMediaAudioSampleTypeInt16Deprecated ||
+         audio_sample_type == kSbMediaAudioSampleTypeFloat32;
+}
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.cc b/src/starboard/android/shared/audio_track_audio_sink_type.cc
new file mode 100644
index 0000000..43cf47d
--- /dev/null
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.cc
@@ -0,0 +1,414 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/audio_track_audio_sink_type.h"
+
+#include <algorithm>
+#include <deque>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/mutex.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+// The maximum number of frames that can be written to android audio track per
+// write request. If we don't set this cap for writing frames to audio track,
+// we will repeatedly allocate a large byte array which cannot be consumed by
+// audio track completely.
+const int kMaxFramesPerRequest = 65536;
+
+const jint kNoOffset = 0;
+
+// Helper function to compute the size of the two valid starboard audio sample
+// types.
+size_t GetSampleSize(SbMediaAudioSampleType sample_type) {
+  switch (sample_type) {
+    case kSbMediaAudioSampleTypeFloat32:
+      return sizeof(float);
+    case kSbMediaAudioSampleTypeInt16Deprecated:
+      return sizeof(int16_t);
+  }
+  SB_NOTREACHED();
+  return 0u;
+}
+
+int GetAudioFormatSampleType(SbMediaAudioSampleType sample_type) {
+  switch (sample_type) {
+    case kSbMediaAudioSampleTypeFloat32:
+      // Android AudioFormat.ENCODING_PCM_FLOAT.
+      return 4;
+    case kSbMediaAudioSampleTypeInt16Deprecated:
+      // Android AudioFormat.ENCODING_PCM_16BIT.
+      return 2;
+  }
+  SB_NOTREACHED();
+  return 0u;
+}
+
+void* IncrementPointerByBytes(void* pointer, size_t offset) {
+  return static_cast<uint8_t*>(pointer) + offset;
+}
+
+class AudioTrackAudioSink : public SbAudioSinkPrivate {
+ public:
+  AudioTrackAudioSink(
+      Type* type,
+      int channels,
+      int sampling_frequency_hz,
+      SbMediaAudioSampleType sample_type,
+      SbAudioSinkFrameBuffers frame_buffers,
+      int frames_per_channel,
+      SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+      SbAudioSinkConsumeFramesFunc consume_frame_func,
+      void* context);
+  ~AudioTrackAudioSink() override;
+
+  bool IsAudioTrackValid() const { return j_audio_track_bridge_; }
+  bool IsType(Type* type) override { return type_ == type; }
+  void SetPlaybackRate(double playback_rate) override {
+    SB_DCHECK(playback_rate >= 0.0);
+    if (playback_rate != 0.0 && playback_rate != 1.0) {
+      SB_NOTIMPLEMENTED() << "TODO: Only playback rates of 0.0 and 1.0 are "
+                             "currently supported.";
+      playback_rate = (playback_rate > 0.0) ? 1.0 : 0.0;
+    }
+    ScopedLock lock(mutex_);
+    playback_rate_ = playback_rate;
+  }
+
+  void SetVolume(double volume) override;
+
+ private:
+  static void* ThreadEntryPoint(void* context);
+  void AudioThreadFunc();
+
+  Type* type_;
+  int channels_;
+  int sampling_frequency_hz_;
+  SbMediaAudioSampleType sample_type_;
+  void* frame_buffer_;
+  int frames_per_channel_;
+  SbAudioSinkUpdateSourceStatusFunc update_source_status_func_;
+  SbAudioSinkConsumeFramesFunc consume_frame_func_;
+  void* context_;
+  int last_playback_head_position_;
+  jobject j_audio_track_bridge_;
+  jobject j_audio_data_;
+
+  volatile bool quit_;
+  SbThread audio_out_thread_;
+
+  starboard::Mutex mutex_;
+  double playback_rate_;
+
+  int written_frames_;
+};
+
+AudioTrackAudioSink::AudioTrackAudioSink(
+    Type* type,
+    int channels,
+    int sampling_frequency_hz,
+    SbMediaAudioSampleType sample_type,
+    SbAudioSinkFrameBuffers frame_buffers,
+    int frames_per_channel,
+    SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+    SbAudioSinkConsumeFramesFunc consume_frame_func,
+    void* context)
+    : type_(type),
+      channels_(channels),
+      sampling_frequency_hz_(sampling_frequency_hz),
+      sample_type_(sample_type),
+      frame_buffer_(frame_buffers[0]),
+      frames_per_channel_(frames_per_channel),
+      update_source_status_func_(update_source_status_func),
+      consume_frame_func_(consume_frame_func),
+      context_(context),
+      last_playback_head_position_(0),
+      j_audio_track_bridge_(NULL),
+      j_audio_data_(NULL),
+      quit_(false),
+      audio_out_thread_(kSbThreadInvalid),
+      playback_rate_(1.0f),
+      written_frames_(0) {
+  SB_DCHECK(update_source_status_func_);
+  SB_DCHECK(consume_frame_func_);
+  SB_DCHECK(frame_buffer_);
+  SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(sample_type));
+
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_audio_output_manager(
+      env->CallStarboardObjectMethodOrAbort(
+          "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;"));
+  jobject j_audio_track_bridge = env->CallObjectMethodOrAbort(
+      j_audio_output_manager.Get(), "createAudioTrackBridge",
+      "(IIII)Ldev/cobalt/media/AudioTrackBridge;",
+      GetAudioFormatSampleType(sample_type_), sampling_frequency_hz_, channels_,
+      frames_per_channel);
+  if (!j_audio_track_bridge) {
+    return;
+  }
+  j_audio_track_bridge_ = env->ConvertLocalRefToGlobalRef(j_audio_track_bridge);
+  if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
+    j_audio_data_ = env->NewFloatArray(channels_ * kMaxFramesPerRequest);
+  } else if (sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated) {
+    j_audio_data_ = env->NewByteArray(channels_ * GetSampleSize(sample_type_) *
+                                      kMaxFramesPerRequest);
+  } else {
+    SB_NOTREACHED();
+  }
+  j_audio_data_ = env->ConvertLocalRefToGlobalRef(j_audio_data_);
+
+  audio_out_thread_ = SbThreadCreate(
+      0, kSbThreadPriorityRealTime, kSbThreadNoAffinity, true,
+      "audio_track_audio_out", &AudioTrackAudioSink::ThreadEntryPoint, this);
+  SB_DCHECK(SbThreadIsValid(audio_out_thread_));
+}
+
+AudioTrackAudioSink::~AudioTrackAudioSink() {
+  quit_ = true;
+
+  if (SbThreadIsValid(audio_out_thread_)) {
+    SbThreadJoin(audio_out_thread_, NULL);
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  if (j_audio_track_bridge_) {
+    ScopedLocalJavaRef<jobject> j_audio_output_manager(
+        env->CallStarboardObjectMethodOrAbort(
+            "getAudioOutputManager",
+            "()Ldev/cobalt/media/AudioOutputManager;"));
+    env->CallVoidMethodOrAbort(
+        j_audio_output_manager.Get(), "destroyAudioTrackBridge",
+        "(Ldev/cobalt/media/AudioTrackBridge;)V", j_audio_track_bridge_);
+    env->DeleteGlobalRef(j_audio_track_bridge_);
+    j_audio_track_bridge_ = NULL;
+  }
+
+  if (j_audio_data_) {
+    env->DeleteGlobalRef(j_audio_data_);
+    j_audio_data_ = NULL;
+  }
+}
+
+// static
+void* AudioTrackAudioSink::ThreadEntryPoint(void* context) {
+  SB_DCHECK(context);
+  AudioTrackAudioSink* sink = reinterpret_cast<AudioTrackAudioSink*>(context);
+  sink->AudioThreadFunc();
+
+  return NULL;
+}
+
+void AudioTrackAudioSink::AudioThreadFunc() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V");
+
+  bool was_playing = true;
+
+  while (!quit_) {
+    ScopedLocalJavaRef<jobject> j_audio_timestamp(
+        env->CallObjectMethodOrAbort(j_audio_track_bridge_, "getAudioTimestamp",
+                                     "()Landroid/media/AudioTimestamp;"));
+
+    int playback_head_position =
+        env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "framePosition", "J");
+    SbTime frames_consumed_at =
+        env->GetLongFieldOrAbort(j_audio_timestamp.Get(), "nanoTime", "J") /
+        1000;
+
+    SB_DCHECK(playback_head_position >= last_playback_head_position_);
+
+    playback_head_position =
+        std::max(playback_head_position, last_playback_head_position_);
+    int frames_consumed = playback_head_position - last_playback_head_position_;
+    last_playback_head_position_ = playback_head_position;
+    frames_consumed = std::min(frames_consumed, written_frames_);
+
+    if (frames_consumed != 0) {
+      SB_DCHECK(frames_consumed >= 0);
+      consume_frame_func_(frames_consumed, frames_consumed_at, context_);
+      written_frames_ -= frames_consumed;
+    }
+
+    int frames_in_buffer;
+    int offset_in_frames;
+    bool is_playing;
+    bool is_eos_reached;
+    update_source_status_func_(&frames_in_buffer, &offset_in_frames,
+                               &is_playing, &is_eos_reached, context_);
+
+    {
+      ScopedLock lock(mutex_);
+      if (playback_rate_ == 0.0) {
+        is_playing = false;
+      }
+    }
+
+    if (was_playing && !is_playing) {
+      was_playing = false;
+      env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V");
+    } else if (!was_playing && is_playing) {
+      was_playing = true;
+      env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V");
+    }
+
+    if (!is_playing || frames_in_buffer == 0) {
+      SbThreadSleep(10 * kSbTimeMillisecond);
+      continue;
+    }
+
+    int start_position =
+        (offset_in_frames + written_frames_) % frames_per_channel_;
+    int expected_written_frames = 0;
+    if (frames_per_channel_ > offset_in_frames + written_frames_) {
+      expected_written_frames =
+          std::min(frames_per_channel_ - (offset_in_frames + written_frames_),
+                   frames_in_buffer - written_frames_);
+    } else {
+      expected_written_frames = frames_in_buffer - written_frames_;
+    }
+
+    expected_written_frames =
+        std::min(expected_written_frames, kMaxFramesPerRequest);
+    if (expected_written_frames == 0) {
+      // It is possible that all the frames in buffer are written to the
+      // soundcard, but those are not being consumed.
+      continue;
+    }
+    SB_DCHECK(expected_written_frames > 0);
+    bool written_fully = false;
+
+    if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
+      int expected_written_size = expected_written_frames * channels_;
+      env->SetFloatArrayRegion(
+          static_cast<jfloatArray>(j_audio_data_), kNoOffset,
+          expected_written_size,
+          static_cast<const float*>(IncrementPointerByBytes(
+              frame_buffer_,
+              start_position * channels_ * GetSampleSize(sample_type_))));
+      int written =
+          env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([FI)I",
+                                    j_audio_data_, expected_written_size);
+      SB_DCHECK(written >= 0);
+      SB_DCHECK(written % channels_ == 0);
+      written_frames_ += written / channels_;
+      written_fully = (written == expected_written_frames);
+    } else if (sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated) {
+      int expected_written_size =
+          expected_written_frames * channels_ * GetSampleSize(sample_type_);
+      env->SetByteArrayRegion(
+          static_cast<jbyteArray>(j_audio_data_), kNoOffset,
+          expected_written_size,
+          static_cast<const jbyte*>(IncrementPointerByBytes(
+              frame_buffer_,
+              start_position * channels_ * GetSampleSize(sample_type_))));
+      int written =
+          env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([BI)I",
+                                    j_audio_data_, expected_written_size);
+      SB_DCHECK(written >= 0);
+      SB_DCHECK(written % (channels_ * GetSampleSize(sample_type_)) == 0);
+      written_frames_ += written / (channels_ * GetSampleSize(sample_type_));
+      written_fully = (written == expected_written_frames);
+    } else {
+      SB_NOTREACHED();
+    }
+
+    auto unplayed_frames_in_time =
+        written_frames_ * kSbTimeSecond / sampling_frequency_hz_ -
+        (SbTimeGetMonotonicNow() - frames_consumed_at);
+    // As long as there is enough data in the buffer, run the loop in lower
+    // frequency to avoid taking too much CPU.  Note that the threshold should
+    // be big enough to account for the unstable playback head reported at the
+    // beginning of the playback and during underrun.
+    if (playback_head_position > 0 &&
+        unplayed_frames_in_time > 500 * kSbTimeMillisecond) {
+      SbThreadSleep(40 * kSbTimeMillisecond);
+    } else if (!written_fully) {
+      // Only sleep if the buffer is nearly full and the last write is partial.
+      SbThreadSleep(1 * kSbTimeMillisecond);
+    }
+  }
+
+  // For an immediate stop, use pause(), followed by flush() to discard audio
+  // data that hasn't been played back yet.
+  env->CallVoidMethodOrAbort(j_audio_track_bridge_, "pause", "()V");
+  // Flushes the audio data currently queued for playback. Any data that has
+  // been written but not yet presented will be discarded.
+  env->CallVoidMethodOrAbort(j_audio_track_bridge_, "flush", "()V");
+}
+
+void AudioTrackAudioSink::SetVolume(double volume) {
+  auto* env = JniEnvExt::Get();
+  jint status = env->CallIntMethodOrAbort(j_audio_track_bridge_, "setVolume",
+                                          "(F)I", static_cast<float>(volume));
+  if (status != 0) {
+    SB_LOG(ERROR) << "Failed to set volume";
+  }
+}
+
+}  // namespace
+
+SbAudioSink AudioTrackAudioSinkType::Create(
+    int channels,
+    int sampling_frequency_hz,
+    SbMediaAudioSampleType audio_sample_type,
+    SbMediaAudioFrameStorageType audio_frame_storage_type,
+    SbAudioSinkFrameBuffers frame_buffers,
+    int frames_per_channel,
+    SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+    SbAudioSinkConsumeFramesFunc consume_frames_func,
+    void* context) {
+  AudioTrackAudioSink* audio_sink = new AudioTrackAudioSink(
+      this, channels, sampling_frequency_hz, audio_sample_type, frame_buffers,
+      frames_per_channel, update_source_status_func, consume_frames_func,
+      context);
+  if (!audio_sink->IsAudioTrackValid()) {
+    SB_DLOG(ERROR)
+        << "AudioTrackAudioSinkType::Create failed to create audio track";
+    Destroy(audio_sink);
+    return kSbAudioSinkInvalid;
+  }
+  return audio_sink;
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+namespace {
+SbAudioSinkPrivate::Type* audio_track_audio_sink_type_;
+}  // namespace
+
+// static
+void SbAudioSinkPrivate::PlatformInitialize() {
+  SB_DCHECK(!audio_track_audio_sink_type_);
+  audio_track_audio_sink_type_ =
+      new starboard::android::shared::AudioTrackAudioSinkType;
+  SetPrimaryType(audio_track_audio_sink_type_);
+  EnableFallbackToStub();
+}
+
+// static
+void SbAudioSinkPrivate::PlatformTearDown() {
+  SB_DCHECK(audio_track_audio_sink_type_ == GetPrimaryType());
+  SetPrimaryType(NULL);
+  delete audio_track_audio_sink_type_;
+  audio_track_audio_sink_type_ = NULL;
+}
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.h b/src/starboard/android/shared/audio_track_audio_sink_type.h
new file mode 100644
index 0000000..6dda67e
--- /dev/null
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.h
@@ -0,0 +1,58 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
+#define STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
+
+#include "starboard/audio_sink.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/audio_sink/audio_sink_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class AudioTrackAudioSinkType : public SbAudioSinkPrivate::Type {
+ public:
+  SbAudioSink Create(
+      int channels,
+      int sampling_frequency_hz,
+      SbMediaAudioSampleType audio_sample_type,
+      SbMediaAudioFrameStorageType audio_frame_storage_type,
+      SbAudioSinkFrameBuffers frame_buffers,
+      int frames_per_channel,
+      SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
+      SbAudioSinkConsumeFramesFunc consume_frames_func,
+      void* context);
+
+  bool IsValid(SbAudioSink audio_sink) override {
+    return audio_sink != kSbAudioSinkInvalid && audio_sink->IsType(this);
+  }
+
+  void Destroy(SbAudioSink audio_sink) override {
+    if (audio_sink != kSbAudioSinkInvalid && !IsValid(audio_sink)) {
+      SB_LOG(WARNING) << "audio_sink is invalid.";
+      return;
+    }
+    delete audio_sink;
+  }
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_AUDIO_TRACK_AUDIO_SINK_TYPE_H_
diff --git a/src/starboard/android/shared/cobalt/android.cc b/src/starboard/android/shared/cobalt/android.cc
new file mode 100644
index 0000000..ab1baa1
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.cc
@@ -0,0 +1,33 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+Android::Android(const scoped_refptr<::cobalt::dom::Window>& window) {
+  UNREFERENCED_PARAMETER(window);
+
+  feedback_service_ = new FeedbackService();
+}
+
+void Android::TraceMembers(script::Tracer* tracer) {
+  tracer->Trace(feedback_service_);
+}
+
+Android::~Android() {}
+
+}  // namespace webapi_extension
+}  // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android.h b/src/starboard/android/shared/cobalt/android.h
new file mode 100644
index 0000000..b21d533
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.h
@@ -0,0 +1,47 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
+
+#include "cobalt/dom/window.h"
+#include "cobalt/script/wrappable.h"
+#include "starboard/android/shared/cobalt/feedback_service.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+class Android : public ::cobalt::script::Wrappable {
+ public:
+  explicit Android(const scoped_refptr<::cobalt::dom::Window>& window);
+
+  const scoped_refptr<FeedbackService>& feedback_service() const {
+    return feedback_service_;
+  }
+
+  DEFINE_WRAPPABLE_TYPE(Android);
+  void TraceMembers(script::Tracer* tracer) override;
+
+ private:
+  ~Android() override;
+
+  scoped_refptr<FeedbackService> feedback_service_;
+
+  DISALLOW_COPY_AND_ASSIGN(Android);
+};
+
+}  // namespace webapi_extension
+}  // namespace cobalt
+
+#endif  // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_H_
diff --git a/src/starboard/android/shared/cobalt/android.idl b/src/starboard/android/shared/cobalt/android.idl
new file mode 100644
index 0000000..18e71e7
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android.idl
@@ -0,0 +1,18 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Cobalt Web Extension for Android.
+interface Android {
+  readonly attribute FeedbackService feedbackService;
+};
diff --git a/src/starboard/android/shared/cobalt/android_media_session_client.cc b/src/starboard/android/shared/cobalt/android_media_session_client.cc
new file mode 100644
index 0000000..25b9323
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_media_session_client.cc
@@ -0,0 +1,355 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+
+#include "base/memory/scoped_ptr.h"
+#include "base/time.h"
+#include "cobalt/media_session/media_session_action_details.h"
+#include "cobalt/media_session/media_session_client.h"
+#include "cobalt/script/sequence.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/once.h"
+#include "starboard/player.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+using ::cobalt::media_session::MediaImage;
+using ::cobalt::media_session::MediaMetadata;
+using ::cobalt::media_session::MediaSession;
+using ::cobalt::media_session::MediaSessionAction;
+using ::cobalt::media_session::MediaSessionActionDetails;
+using ::cobalt::media_session::MediaSessionClient;
+using ::cobalt::media_session::MediaSessionPlaybackState;
+using ::cobalt::media_session::kMediaSessionActionPause;
+using ::cobalt::media_session::kMediaSessionActionPlay;
+using ::cobalt::media_session::kMediaSessionActionSeek;
+using ::cobalt::media_session::kMediaSessionActionSeekbackward;
+using ::cobalt::media_session::kMediaSessionActionSeekforward;
+using ::cobalt::media_session::kMediaSessionActionPrevioustrack;
+using ::cobalt::media_session::kMediaSessionActionNexttrack;
+using ::cobalt::media_session::kMediaSessionPlaybackStateNone;
+using ::cobalt::media_session::kMediaSessionPlaybackStatePaused;
+using ::cobalt::media_session::kMediaSessionPlaybackStatePlaying;
+
+using MediaImageSequence = ::cobalt::script::Sequence<MediaImage>;
+
+using ::starboard::android::shared::JniEnvExt;
+using ::starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+// These constants are from android.media.session.PlaybackState
+const jlong kPlaybackStateActionStop = 1 << 0;  // not supported
+const jlong kPlaybackStateActionPause = 1 << 1;
+const jlong kPlaybackStateActionPlay = 1 << 2;
+const jlong kPlaybackStateActionRewind = 1 << 3;
+const jlong kPlaybackStateActionSkipToPrevious = 1 << 4;
+const jlong kPlaybackStateActionSkipToNext = 1 << 5;
+const jlong kPlaybackStateActionFastForward = 1 << 6;
+const jlong kPlaybackStateActionSetRating = 1 << 7;  // not supported
+const jlong kPlaybackStateActionSeekTo = 1 << 8;
+
+// Converts a MediaSessionClient::AvailableActions bitset into
+// a android.media.session.PlaybackState jlong bitset.
+jlong MediaSessionActionsToPlaybackStateActions(
+    const MediaSessionClient::AvailableActionsSet& actions) {
+  jlong result = 0;
+  if (actions[kMediaSessionActionPause]) {
+    result |= kPlaybackStateActionPause;
+  }
+  if (actions[kMediaSessionActionPlay]) {
+    result |= kPlaybackStateActionPlay;
+  }
+  if (actions[kMediaSessionActionSeekbackward]) {
+    result |= kPlaybackStateActionRewind;
+  }
+  if (actions[kMediaSessionActionPrevioustrack]) {
+    result |= kPlaybackStateActionSkipToPrevious;
+  }
+  if (actions[kMediaSessionActionNexttrack]) {
+    result |= kPlaybackStateActionSkipToNext;
+  }
+  if (actions[kMediaSessionActionSeekforward]) {
+    result |= kPlaybackStateActionFastForward;
+  }
+  if (actions[kMediaSessionActionSeek]) {
+    result |= kPlaybackStateActionSeekTo;
+  }
+  return result;
+}
+
+PlaybackState MediaSessionPlaybackStateToPlaybackState(
+    MediaSessionPlaybackState in_state) {
+  switch (in_state) {
+    case kMediaSessionPlaybackStatePlaying:
+      return kPlaying;
+    case kMediaSessionPlaybackStatePaused:
+      return kPaused;
+    case kMediaSessionPlaybackStateNone:
+      return kNone;
+  }
+}
+
+MediaSessionAction PlaybackStateActionToMediaSessionAction(jlong action) {
+  MediaSessionAction result;
+  switch (action) {
+    case kPlaybackStateActionPause:
+      result = kMediaSessionActionPause;
+      break;
+    case kPlaybackStateActionPlay:
+      result = kMediaSessionActionPlay;
+      break;
+    case kPlaybackStateActionRewind:
+      result = kMediaSessionActionSeekbackward;
+      break;
+    case kPlaybackStateActionSkipToPrevious:
+      result = kMediaSessionActionPrevioustrack;
+      break;
+    case kPlaybackStateActionSkipToNext:
+      result = kMediaSessionActionNexttrack;
+      break;
+    case kPlaybackStateActionFastForward:
+      result = kMediaSessionActionSeekforward;
+      break;
+    case kPlaybackStateActionSeekTo:
+      result = kMediaSessionActionSeek;
+      break;
+    default:
+      SB_NOTREACHED() << "Unsupported MediaSessionAction 0x"
+                      << std::hex << action;
+      result = static_cast<MediaSessionAction>(-1);
+  }
+  return result;
+}
+
+MediaSessionPlaybackState PlaybackStateToMediaSessionPlaybackState(
+    PlaybackState state) {
+  MediaSessionPlaybackState result;
+  switch (state) {
+    case kPlaying:
+      result = kMediaSessionPlaybackStatePlaying;
+      break;
+    case kPaused:
+      result = kMediaSessionPlaybackStatePaused;
+      break;
+    case kNone:
+      result = kMediaSessionPlaybackStateNone;
+      break;
+    default:
+      SB_NOTREACHED() << "Unsupported PlaybackState " << state;
+      result = static_cast<MediaSessionPlaybackState>(-1);
+  }
+  return result;
+}
+
+}  // namespace
+
+class AndroidMediaSessionClient : public MediaSessionClient {
+  static SbOnceControl once_flag;
+  static SbMutex mutex;
+  // The last MediaSessionClient to become active, or null.
+  // Used to route Java callbacks.
+  // In practice, only one MediaSessionClient will become active at a time.
+  // Protected by "mutex"
+  static AndroidMediaSessionClient* active_client;
+
+  // TODO: Pass the necessary info through web MediaSession so we don't need to
+  // short-circuit to the player implementation to get info about the playback.
+  static SbPlayer active_player;
+
+  static void OnceInit() { SbMutexCreate(&mutex); }
+
+ public:
+  static void NativeInvokeAction(jlong action, jlong seek_ms) {
+    SbOnce(&once_flag, OnceInit);
+    SbMutexAcquire(&mutex);
+
+    if (active_client != NULL) {
+      MediaSessionAction cobalt_action =
+          PlaybackStateActionToMediaSessionAction(action);
+      active_client->InvokeAction(scoped_ptr<MediaSessionActionDetails::Data>(
+          new MediaSessionActionDetails::Data(cobalt_action,
+                                              seek_ms / 1000.0)));
+    }
+
+    SbMutexRelease(&mutex);
+  }
+
+  static void UpdateActiveSessionPlatformPlaybackState(
+      MediaSessionPlaybackState state) {
+    SbOnce(&once_flag, OnceInit);
+    SbMutexAcquire(&mutex);
+
+    if (active_client != NULL) {
+      active_client->UpdatePlatformPlaybackState(state);
+    }
+
+    SbMutexRelease(&mutex);
+  }
+
+  static void UpdateActiveSessionPlatformPlayer(SbPlayer player) {
+    SbOnce(&once_flag, OnceInit);
+    SbMutexAcquire(&mutex);
+
+    active_player = player;
+
+    SbMutexRelease(&mutex);
+  }
+
+  AndroidMediaSessionClient() {}
+
+  virtual ~AndroidMediaSessionClient() {
+    SbOnce(&once_flag, OnceInit);
+    SbMutexAcquire(&mutex);
+    if (active_client == this) {
+      active_client = NULL;
+    }
+    SbMutexRelease(&mutex);
+  }
+
+  void OnMediaSessionChanged() override {
+    JniEnvExt* env = JniEnvExt::Get();
+
+    jint playback_state =
+        MediaSessionPlaybackStateToPlaybackState(GetActualPlaybackState());
+
+    SbOnce(&once_flag, OnceInit);
+    SbMutexAcquire(&mutex);
+    if (playback_state != kNone) {
+      active_client = this;
+    } else if (active_client == this) {
+      active_client = NULL;
+    }
+
+    SbPlayerInfo2 player_info;
+    SbMemorySet(&player_info, 0, sizeof(player_info));
+    if (active_player != kSbPlayerInvalid) {
+      SbPlayerGetInfo2(active_player, &player_info);
+    }
+    SbMutexRelease(&mutex);
+
+    jlong playback_state_actions =
+        MediaSessionActionsToPlaybackStateActions(GetAvailableActions());
+
+    scoped_refptr<MediaSession> media_session(GetMediaSession());
+    scoped_refptr<MediaMetadata> media_metadata(media_session->metadata());
+
+    ScopedLocalJavaRef<jstring> j_title;
+    ScopedLocalJavaRef<jstring> j_artist;
+    ScopedLocalJavaRef<jstring> j_album;
+    ScopedLocalJavaRef<jobjectArray> j_artwork;
+
+    if (media_metadata) {
+      j_title.Reset(
+          env->NewStringStandardUTFOrAbort(media_metadata->title().c_str()));
+      j_artist.Reset(
+          env->NewStringStandardUTFOrAbort(media_metadata->artist().c_str()));
+      j_album.Reset(
+          env->NewStringStandardUTFOrAbort(media_metadata->album().c_str()));
+
+      const MediaImageSequence& artwork(media_metadata->artwork());
+      if (!artwork.empty()) {
+        ScopedLocalJavaRef<jclass> media_image_class(
+            env->FindClassExtOrAbort("dev/cobalt/media/MediaImage"));
+        jmethodID media_image_constructor = env->GetMethodID(
+            media_image_class.Get(), "<init>",
+            "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+        env->AbortOnException();
+
+        j_artwork.Reset(static_cast<jobjectArray>(env->NewObjectArray(
+            artwork.size(), media_image_class.Get(), NULL)));
+        env->AbortOnException();
+
+        ScopedLocalJavaRef<jstring> j_src;
+        ScopedLocalJavaRef<jstring> j_sizes;
+        ScopedLocalJavaRef<jstring> j_type;
+        for (MediaImageSequence::size_type i = 0; i < artwork.size(); i++) {
+          const MediaImage& media_image(artwork.at(i));
+          j_src.Reset(
+              env->NewStringStandardUTFOrAbort(media_image.src().c_str()));
+          j_sizes.Reset(
+              env->NewStringStandardUTFOrAbort(media_image.sizes().c_str()));
+          j_type.Reset(
+              env->NewStringStandardUTFOrAbort(media_image.type().c_str()));
+
+          ScopedLocalJavaRef<jobject> j_media_image(
+              env->NewObject(media_image_class.Get(), media_image_constructor,
+                             j_src.Get(), j_sizes.Get(), j_type.Get()));
+
+          env->SetObjectArrayElement(j_artwork.Get(), i, j_media_image.Get());
+        }
+      }
+    }
+
+    env->CallStarboardVoidMethodOrAbort(
+        "updateMediaSession",
+        "(IJJFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;"
+            "[Ldev/cobalt/media/MediaImage;)V",
+        playback_state, playback_state_actions,
+        player_info.current_media_timestamp / kSbTimeMillisecond,
+        static_cast<jfloat>(player_info.playback_rate),
+        j_title.Get(), j_artist.Get(), j_album.Get(), j_artwork.Get());
+  }
+};
+
+SbOnceControl AndroidMediaSessionClient::once_flag = SB_ONCE_INITIALIZER;
+SbMutex AndroidMediaSessionClient::mutex;
+AndroidMediaSessionClient* AndroidMediaSessionClient::active_client = NULL;
+SbPlayer AndroidMediaSessionClient::active_player = kSbPlayerInvalid;
+
+void UpdateActiveSessionPlatformPlaybackState(PlaybackState state) {
+  MediaSessionPlaybackState media_session_state =
+      PlaybackStateToMediaSessionPlaybackState(state);
+
+  AndroidMediaSessionClient::UpdateActiveSessionPlatformPlaybackState(
+      media_session_state);
+}
+
+void UpdateActiveSessionPlatformPlayer(SbPlayer player) {
+  AndroidMediaSessionClient::UpdateActiveSessionPlatformPlayer(player);
+}
+
+}  // namespace cobalt
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+using starboard::android::shared::cobalt::AndroidMediaSessionClient;
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_media_CobaltMediaSession_nativeInvokeAction(
+    JNIEnv* env,
+    jclass unused_clazz,
+    jlong action,
+    jlong seek_ms) {
+  AndroidMediaSessionClient::NativeInvokeAction(action, seek_ms);
+}
+
+namespace cobalt {
+namespace media_session {
+
+// static
+scoped_ptr<MediaSessionClient> MediaSessionClient::Create() {
+  return make_scoped_ptr<MediaSessionClient>(new AndroidMediaSessionClient());
+}
+
+}  // namespace media_session
+}  // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_media_session_client.h b/src/starboard/android/shared/cobalt/android_media_session_client.h
new file mode 100644
index 0000000..284e165
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_media_session_client.h
@@ -0,0 +1,39 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
+
+#include "starboard/player.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+// Duplicated in CobaltMediaSession.java
+enum PlaybackState { kPlaying = 0, kPaused = 1, kNone = 2 };
+
+void UpdateActiveSessionPlatformPlaybackState(PlaybackState state);
+
+// TODO: Pass the necessary info through web MediaSession so we don't need to
+// short-circuit to the player implementation to get info about the playback.
+void UpdateActiveSessionPlatformPlayer(SbPlayer player);
+
+}  // namespace cobalt
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_MEDIA_SESSION_CLIENT_H_
diff --git a/src/starboard/android/shared/cobalt/android_user_authorizer.cc b/src/starboard/android/shared/cobalt/android_user_authorizer.cc
new file mode 100644
index 0000000..a84b653
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_user_authorizer.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/android_user_authorizer.h"
+
+#include "base/time.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+
+// We're in starboard source, but compiled with Cobalt.
+#define STARBOARD_IMPLEMENTATION
+#include "starboard/shared/nouser/user_internal.h"
+#undef STARBOARD_IMPLEMENTATION
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+AndroidUserAuthorizer::AndroidUserAuthorizer() : shutdown_(false) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject local_ref = env->CallStarboardObjectMethodOrAbort(
+      "getUserAuthorizer", "()Ldev/cobalt/account/UserAuthorizer;");
+  j_user_authorizer_ = env->ConvertLocalRefToGlobalRef(local_ref);
+}
+
+AndroidUserAuthorizer::~AndroidUserAuthorizer() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->DeleteGlobalRef(j_user_authorizer_);
+}
+
+void AndroidUserAuthorizer::Shutdown() {
+  shutdown_ = true;
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(j_user_authorizer_, "interrupt", "()V");
+}
+
+scoped_ptr<AccessToken> AndroidUserAuthorizer::AuthorizeUser(SbUser user) {
+  SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+  if (shutdown_) {
+    DLOG(WARNING) << "No-op AuthorizeUser after shutdown";
+    return scoped_ptr<AccessToken>(NULL);
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_token(
+      env->CallObjectMethodOrAbort(j_user_authorizer_, "authorizeUser",
+                                   "()Ldev/cobalt/account/AccessToken;"));
+  return CreateAccessToken(j_token.Get());
+}
+
+bool AndroidUserAuthorizer::DeauthorizeUser(SbUser user) {
+  SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+  if (shutdown_) {
+    DLOG(WARNING) << "No-op DeauthorizeUser after shutdown";
+    return false;
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  return env->CallBooleanMethodOrAbort(j_user_authorizer_, "deauthorizeUser",
+                                       "()Z");
+}
+
+scoped_ptr<AccessToken>
+AndroidUserAuthorizer::RefreshAuthorization(SbUser user) {
+  SB_DCHECK(user == &::starboard::shared::nouser::g_user);
+  if (shutdown_) {
+    DLOG(WARNING) << "No-op RefreshAuthorization after shutdown";
+    return scoped_ptr<AccessToken>(NULL);
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_token(
+      env->CallObjectMethodOrAbort(j_user_authorizer_, "refreshAuthorization",
+                                   "()Ldev/cobalt/account/AccessToken;"));
+  return CreateAccessToken(j_token.Get());
+}
+
+scoped_ptr<AccessToken>
+AndroidUserAuthorizer::CreateAccessToken(jobject j_token) {
+  if (!j_token) {
+    return scoped_ptr<AccessToken>(NULL);
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  scoped_ptr<AccessToken> access_token(new AccessToken());
+
+  ScopedLocalJavaRef<jstring> j_token_string(env->CallObjectMethodOrAbort(
+      j_token, "getTokenValue", "()Ljava/lang/String;"));
+  if (j_token_string) {
+    access_token->token_value =
+        env->GetStringStandardUTFOrAbort(j_token_string.Get());
+  }
+
+  jlong j_expiry = env->CallLongMethodOrAbort(
+      j_token, "getExpirySeconds", "()J");
+  if (j_expiry > 0) {
+    access_token->expiry =
+        base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(j_expiry);
+  }
+
+  return access_token.Pass();
+}
+
+}  // namespace cobalt
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+namespace cobalt {
+namespace account {
+
+UserAuthorizer* UserAuthorizer::Create() {
+  return new ::starboard::android::shared::cobalt::AndroidUserAuthorizer();
+}
+
+}  // namespace account
+}  // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_user_authorizer.h b/src/starboard/android/shared/cobalt/android_user_authorizer.h
new file mode 100644
index 0000000..6e9cadc
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_user_authorizer.h
@@ -0,0 +1,63 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
+
+#include "cobalt/account/user_authorizer.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace cobalt {
+
+using ::cobalt::account::AccessToken;
+
+// Android implementation of UserAuthorizer, using the single 'nouser' SbUser to
+// represent the Android platform running our app.  Unlike game consoles which
+// launch the app as a particular platform user, Android always launches the app
+// as the same platform "user" no matter what accounts may be on the device.
+//
+// Signing-in is a higher-level concept that is implemented by the Android app
+// using the Android AccountManager and/or Google Play Services to select an
+// account and get auth tokens for the selected account to make "signed-in"
+// requests.  Account selection is accomplished by prompting the user as needed
+// when getting a token with AuthorizeUser(), and remembering the selected
+// account to be used without prompting in RefreshAuthorization().
+class AndroidUserAuthorizer : public ::cobalt::account::UserAuthorizer {
+ public:
+  AndroidUserAuthorizer();
+  ~AndroidUserAuthorizer() override;
+
+  scoped_ptr<AccessToken> AuthorizeUser(SbUser user) override;
+  bool DeauthorizeUser(SbUser user) override;
+  scoped_ptr<AccessToken> RefreshAuthorization(SbUser user) override;
+  void Shutdown() override;
+
+ private:
+  scoped_ptr<AccessToken> CreateAccessToken(jobject j_token);
+
+  jobject j_user_authorizer_;
+
+  bool shutdown_;
+};
+
+}  // namespace cobalt
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_COBALT_ANDROID_USER_AUTHORIZER_H_
diff --git a/src/starboard/android/shared/cobalt/android_webapi_extension.cc b/src/starboard/android/shared/cobalt/android_webapi_extension.cc
new file mode 100644
index 0000000..827c702
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_webapi_extension.cc
@@ -0,0 +1,38 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/browser/webapi_extension.h"
+
+#include "base/compiler_specific.h"
+#include "cobalt/script/global_environment.h"
+#include "starboard/android/shared/cobalt/android.h"
+
+namespace cobalt {
+namespace browser {
+
+base::optional<std::string> GetWebAPIExtensionObjectPropertyName() {
+  return std::string("android");
+}
+
+scoped_refptr<::cobalt::script::Wrappable> CreateWebAPIExtensionObject(
+    const scoped_refptr<::cobalt::dom::Window>& window,
+    ::cobalt::script::GlobalEnvironment* global_environment) {
+  UNREFERENCED_PARAMETER(global_environment);
+
+  return scoped_refptr<::cobalt::script::Wrappable>(
+      new webapi_extension::Android(window));
+}
+
+}  // namespace browser
+}  // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/android_webapi_extension.gyp b/src/starboard/android/shared/cobalt/android_webapi_extension.gyp
new file mode 100644
index 0000000..0c9511e
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/android_webapi_extension.gyp
@@ -0,0 +1,33 @@
+# Copyright 2018 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'targets': [
+    {
+      'target_name': 'android_webapi_extension',
+      'type': 'static_library',
+      'sources': [
+        'android.h',
+        'android.cc',
+        'android_webapi_extension.cc',
+        'feedback_service.h',
+        'feedback_service.cc',
+      ],
+      'dependencies': [
+        '<(DEPTH)/cobalt/dom/dom.gyp:dom',
+        '<(DEPTH)/cobalt/script/script.gyp:script',
+        '<(DEPTH)/base/base.gyp:base',
+      ],
+    },
+  ],
+}
diff --git a/src/starboard/android/shared/cobalt/cobalt_platform.gyp b/src/starboard/android/shared/cobalt/cobalt_platform.gyp
new file mode 100644
index 0000000..6f6379a
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/cobalt_platform.gyp
@@ -0,0 +1,30 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+  'targets': [
+    {
+      'target_name': 'cobalt_platform',
+      'type': 'static_library',
+      'sources': [
+        'android_user_authorizer.h',
+        'android_user_authorizer.cc',
+        'android_media_session_client.cc',
+      ],
+      'dependencies': [
+        '<(DEPTH)/cobalt/media_session/media_session.gyp:media_session'
+      ],
+    },
+  ],
+}
diff --git a/src/starboard/android/shared/cobalt/configuration.gypi b/src/starboard/android/shared/cobalt/configuration.gypi
new file mode 100644
index 0000000..35647ce
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/configuration.gypi
@@ -0,0 +1,62 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Cobalt-on-Android-specific configuration for Starboard.
+
+{
+  'variables': {
+    'in_app_dial': 0,
+
+    'custom_media_session_client': 1,
+    'enable_account_manager': 1,
+    'enable_map_to_mesh': 1,
+
+    # Some Android devices do not advertise their support for
+    # EGL_SWAP_BEHAVIOR_PRESERVED_BIT properly, so, we play it safe and disable
+    # relying on it for Android.
+    'render_dirty_region_only': 0,
+
+    # The 'android_system' font package installs only minimal fonts, with a
+    # fonts.xml referencing the superset of font files we expect to find on any
+    # Android platform. The Android SbFileOpen implementation falls back to
+    # system fonts when it can't find the font file in the cobalt content.
+    'cobalt_font_package': 'android_system',
+
+    # On Android, we almost never want to actually terminate the process, so
+    # instead indicate that we would instead like to be suspended when users
+    # initiate an "exit".
+    'cobalt_user_on_exit_strategy': 'suspend',
+
+    # Switch Android's SurfaceFlinger queue to "async mode" so that we don't
+    # queue up rendered frames which would interfere with frame timing and
+    # more importantly lead to input latency.
+    'cobalt_egl_swap_interval': 0,
+
+    # Platform-specific implementations to compile into cobalt.
+    'cobalt_platform_dependencies': [
+      '<(DEPTH)/starboard/android/shared/cobalt/cobalt_platform.gyp:cobalt_platform',
+    ],
+
+    # Platform-specific interfaces to inject into Cobalt's JavaScript 'window'
+    # global object.
+    'cobalt_webapi_extension_source_idl_files': [
+      'android.idl',
+      'feedback_service.idl',
+    ],
+
+    # Platform-specific IDL interface implementations.
+    'cobalt_webapi_extension_gyp_target':
+    '<(DEPTH)/starboard/android/shared/cobalt/android_webapi_extension.gyp:android_webapi_extension',
+  },
+}
diff --git a/src/starboard/android/shared/cobalt/configuration.py b/src/starboard/android/shared/cobalt/configuration.py
new file mode 100644
index 0000000..f1121be
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/configuration.py
@@ -0,0 +1,69 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android Cobalt shared configuration."""
+
+import os
+
+from cobalt.build import cobalt_configuration
+from starboard.tools.testing import test_filter
+
+
+class CobaltAndroidConfiguration(cobalt_configuration.CobaltConfiguration):
+  """Starboard Android Cobalt shared configuration."""
+
+  def GetPostIncludes(self):
+    # If there isn't a configuration.gypi found in the usual place, we'll
+    # supplement with our shared implementation.
+    includes = super(CobaltAndroidConfiguration, self).GetPostIncludes()
+    for include in includes:
+      if os.path.basename(include) == 'configuration.gypi':
+        return includes
+
+    shared_gypi_path = os.path.join(os.path.dirname(__file__),
+                                    'configuration.gypi')
+    if os.path.isfile(shared_gypi_path):
+      includes.append(shared_gypi_path)
+    return includes
+
+  def WebdriverBenchmarksEnabled(self):
+    return True
+
+  def GetTestFilters(self):
+    filters = super(CobaltAndroidConfiguration, self).GetTestFilters()
+    for target, tests in self._FAILING_TESTS.iteritems():
+      filters.extend(test_filter.TestFilter(target, test) for test in tests)
+    return filters
+
+  def GetTestEnvVariables(self):
+    return {
+        'base_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'},
+        'crypto_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'},
+        'net_unittests': {'ASAN_OPTIONS': 'detect_leaks=0'}
+    }
+
+  # A map of failing or crashing tests per target.
+  _FAILING_TESTS = {
+      'net_unittests': [
+          # This test fails on multiple platforms with our current version of
+          # net library, and will be fixed when net is rebased.
+          'HostResolverImplDnsTest.DnsTaskUnspec',
+      ],
+      'renderer_test': [
+          # Instead of returning an error when allocating too much texture
+          # memory, Android instead just terminates the process.  Since this
+          # test explicitly tries to allocate too much texture memory, we cannot
+          # run it on Android platforms.
+          'StressTest.TooManyTextures',
+      ],
+  }
diff --git a/src/starboard/android/shared/cobalt/feedback_service.cc b/src/starboard/android/shared/cobalt/feedback_service.cc
new file mode 100644
index 0000000..c17307d
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.cc
@@ -0,0 +1,83 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/cobalt/feedback_service.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+void FeedbackService::SendFeedback(
+    const script::ValueHandleHolder& product_specific_data,
+    const std::string& category_tag,
+    script::ExceptionState* exception_state) {
+  const script::Handle<script::ArrayBuffer> empty_array_buffer;
+  SendFeedback(product_specific_data, category_tag, empty_array_buffer,
+               exception_state);
+}
+
+void FeedbackService::SendFeedback(
+    const script::ValueHandleHolder& product_specific_data,
+    const std::string& category_tag,
+    const script::Handle<script::ArrayBuffer>& screenshot_data,
+    script::ExceptionState* exception_state) {
+  using starboard::android::shared::ScopedLocalJavaRef;
+  using starboard::android::shared::JniEnvExt;
+
+  std::unordered_map<std::string, std::string> product_specific_data_map =
+      script::ConvertSimpleObjectToMap(product_specific_data, exception_state);
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  // Convert the unordered map of product specific data to a hashmap in JNI.
+  ScopedLocalJavaRef<jobject> product_specific_data_hash_map(
+      env->NewObjectOrAbort("java/util/HashMap", "(I)V",
+                            product_specific_data_map.size()));
+
+  ScopedLocalJavaRef<jstring> key;
+  ScopedLocalJavaRef<jstring> value;
+
+  for (const auto& data : product_specific_data_map) {
+    key.Reset(env->NewStringStandardUTFOrAbort(data.first.c_str()));
+    value.Reset(env->NewStringStandardUTFOrAbort(data.second.c_str()));
+
+    env->CallObjectMethodOrAbort(
+        product_specific_data_hash_map.Get(), "put",
+        "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", key.Get(),
+        value.Get());
+  }
+
+  ScopedLocalJavaRef<jstring> category_tag_string;
+  category_tag_string.Reset(
+      env->NewStringStandardUTFOrAbort(category_tag.c_str()));
+
+  // Convert the screenshot to a byte array in JNI.
+  ScopedLocalJavaRef<jbyteArray> screenshot_byte_array;
+  if (!screenshot_data.IsEmpty()) {
+    screenshot_byte_array.Reset(env->NewByteArrayFromRaw(
+        reinterpret_cast<const jbyte*>(screenshot_data->Data()),
+        screenshot_data->ByteLength()));
+    env->AbortOnException();
+  }
+
+  env->CallStarboardVoidMethodOrAbort(
+      "sendFeedback", "(Ljava/util/HashMap;Ljava/lang/String;[B)V",
+      product_specific_data_hash_map.Get(), category_tag_string.Get(),
+      screenshot_byte_array.Get());
+}
+
+}  // namespace webapi_extension
+}  // namespace cobalt
diff --git a/src/starboard/android/shared/cobalt/feedback_service.h b/src/starboard/android/shared/cobalt/feedback_service.h
new file mode 100644
index 0000000..270cf16
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.h
@@ -0,0 +1,49 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
+#define STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
+
+#include <string>
+
+#include "cobalt/script/array_buffer.h"
+#include "cobalt/script/exception_state.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/script/wrappable.h"
+
+namespace cobalt {
+namespace webapi_extension {
+
+class FeedbackService : public ::cobalt::script::Wrappable {
+ public:
+  void SendFeedback(const script::ValueHandleHolder& product_specific_data,
+                    const std::string& category_tag,
+                    script::ExceptionState* exception_state);
+
+  void SendFeedback(const script::ValueHandleHolder& product_specific_data,
+                    const std::string& category_tag,
+                    const script::Handle<script::ArrayBuffer>& screenshot_data,
+                    script::ExceptionState* exception_state);
+
+  FeedbackService() = default;
+  FeedbackService(const FeedbackService&) = delete;
+  FeedbackService& operator=(const FeedbackService&) = delete;
+
+  DEFINE_WRAPPABLE_TYPE(FeedbackService);
+};
+
+}  // namespace webapi_extension
+}  // namespace cobalt
+
+#endif  // STARBOARD_ANDROID_SHARED_COBALT_FEEDBACK_SERVICE_H_
diff --git a/src/starboard/android/shared/cobalt/feedback_service.idl b/src/starboard/android/shared/cobalt/feedback_service.idl
new file mode 100644
index 0000000..b4c722a
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/feedback_service.idl
@@ -0,0 +1,21 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The send feedback report service.
+interface FeedbackService {
+  // Send feedback.
+  [RaisesException] void sendFeedback(any productSpecificData,
+                                      optional DOMString categoryTag = "",
+                                      optional ArrayBuffer screenshotData);
+};
diff --git a/src/starboard/android/shared/configuration_public.h b/src/starboard/android/shared/configuration_public.h
new file mode 100644
index 0000000..79203a7
--- /dev/null
+++ b/src/starboard/android/shared/configuration_public.h
@@ -0,0 +1,455 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for generic Android. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android -- but they may base their configuration on the generic
+// Android configuration headers.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#ifndef STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
+
+// The API version implemented by this platform.
+#define SB_API_VERSION SB_EXPERIMENTAL_API_VERSION
+
+// --- Architecture Configuration --------------------------------------------
+
+// Whether the current platform is big endian. SB_IS_LITTLE_ENDIAN will be
+// automatically set based on this.
+#define SB_IS_BIG_ENDIAN 0
+
+// Whether the current platform is a MIPS architecture.
+#define SB_IS_ARCH_MIPS 0
+
+// Whether the current platform is a PPC architecture.
+#define SB_IS_ARCH_PPC 0
+
+// The current platform CPU architecture architecture.
+#if defined(__arm__) || defined(__aarch64__)
+#define SB_IS_ARCH_ARM 1
+#define SB_IS_ARCH_X86 0
+#elif defined(__i386__) || defined(__x86_64__)
+#define SB_IS_ARCH_ARM 0
+#define SB_IS_ARCH_X86 1
+#endif
+
+// Whether the current platform is 32-bit or 64-bit architecture.
+#if defined(__aarch64__) || defined(__x86_64__)
+#define SB_IS_32_BIT 0
+#define SB_IS_64_BIT 1
+#else
+#define SB_IS_32_BIT 1
+#define SB_IS_64_BIT 0
+#endif
+
+// Whether the current platform's pointers are 32-bit.
+// Whether the current platform's longs are 32-bit.
+#if SB_IS(32_BIT)
+#define SB_HAS_32_BIT_POINTERS 1
+#define SB_HAS_32_BIT_LONG 1
+#else
+#define SB_HAS_32_BIT_POINTERS 0
+#define SB_HAS_32_BIT_LONG 0
+#endif
+
+// Whether the current platform's pointers are 64-bit.
+// Whether the current platform's longs are 64-bit.
+#if SB_IS(64_BIT)
+#define SB_HAS_64_BIT_POINTERS 1
+#define SB_HAS_64_BIT_LONG 1
+#else
+#define SB_HAS_64_BIT_POINTERS 0
+#define SB_HAS_64_BIT_LONG 0
+#endif
+
+// Configuration parameters that allow the application to make some general
+// compile-time decisions with respect to the the number of cores likely to be
+// available on this platform. For a definitive measure, the application should
+// still call SbSystemGetNumberOfProcessors at runtime.
+
+// Whether the current platform is expected to have many cores (> 6), or a
+// wildly varying number of cores.
+#define SB_HAS_MANY_CORES 1
+
+// Whether the current platform is expected to have exactly 1 core.
+#define SB_HAS_1_CORE 0
+
+// Whether the current platform is expected to have exactly 2 cores.
+#define SB_HAS_2_CORES 0
+
+// Whether the current platform is expected to have exactly 4 cores.
+#define SB_HAS_4_CORES 0
+
+// Whether the current platform is expected to have exactly 6 cores.
+#define SB_HAS_6_CORES 0
+
+// Whether the current platform's thread scheduler will automatically balance
+// threads between cores, as opposed to systems where threads will only ever run
+// on the specifically pinned core.
+#define SB_HAS_CROSS_CORE_SCHEDULER 1
+
+// Indicates that there is no support for alignment at greater than 16 bytes for
+// items on the stack.
+#define SB_HAS_QUIRK_DOES_NOT_STACK_ALIGN_OVER_16_BYTES 1
+
+// This quirk is used to fix an issue caused by the rewriting of memset to
+// SbMemorySet in third_party/protobuf/src/google/protobuf/stubs/port.h.
+#define SB_HAS_QUIRK_MEMSET_IN_SYSTEM_HEADERS 1
+
+// --- System Header Configuration -------------------------------------------
+
+// Any system headers listed here that are not provided by the platform will be
+// emulated in starboard/types.h.
+
+// Whether the current platform provides the standard header stdarg.h.
+#define SB_HAS_STDARG_H 1
+
+// Whether the current platform provides the standard header stdbool.h.
+#define SB_HAS_STDBOOL_H 1
+
+// Whether the current platform provides the standard header stddef.h.
+#define SB_HAS_STDDEF_H 1
+
+// Whether the current platform provides the standard header stdint.h.
+#define SB_HAS_STDINT_H 1
+
+// Whether the current platform provides the standard header inttypes.h.
+#define SB_HAS_INTTYPES_H 1
+
+// Whether the current platform provides the standard header wchar.h.
+#define SB_HAS_WCHAR_H 1
+
+// Whether the current platform provides the standard header limits.h.
+#define SB_HAS_LIMITS_H 1
+
+// Whether the current platform provides the standard header float.h.
+#define SB_HAS_FLOAT_H 1
+
+// Whether the current platform provides ssize_t.
+#define SB_HAS_SSIZE_T 1
+
+// Type detection for wchar_t.
+#if defined(__WCHAR_MAX__) && \
+    (__WCHAR_MAX__ == 0x7fffffff || __WCHAR_MAX__ == 0xffffffff)
+#define SB_IS_WCHAR_T_UTF32 1
+#elif defined(__WCHAR_MAX__) && \
+    (__WCHAR_MAX__ == 0x7fff || __WCHAR_MAX__ == 0xffff)
+#define SB_IS_WCHAR_T_UTF16 1
+#endif
+
+// Chrome only defines these two if ARMEL or MIPSEL are defined.
+#if defined(__ARMEL__)
+// Chrome has an exclusion for iOS here, we should too when we support iOS.
+#define SB_IS_WCHAR_T_UNSIGNED 1
+#elif defined(__MIPSEL__)
+#define SB_IS_WCHAR_T_SIGNED 1
+#endif
+
+// --- Compiler Configuration ------------------------------------------------
+
+// The platform's annotation for forcing a C function to be inlined.
+#define SB_C_FORCE_INLINE __inline__ __attribute__((always_inline))
+
+// The platform's annotation for marking a C function as suggested to be
+// inlined.
+#define SB_C_INLINE inline
+
+// The platform's annotation for marking a C function as forcibly not
+// inlined.
+#define SB_C_NOINLINE __attribute__((noinline))
+
+// The platform's annotation for marking a symbol as exported outside of the
+// current shared library.
+#define SB_EXPORT_PLATFORM __attribute__((visibility("default")))
+
+// The platform's annotation for marking a symbol as imported from outside of
+// the current linking unit.
+#define SB_IMPORT_PLATFORM
+
+// --- Extensions Configuration ----------------------------------------------
+
+// GCC/Clang doesn't define a long long hash function, except for Android and
+// Game consoles.
+#define SB_HAS_LONG_LONG_HASH 0
+
+// GCC/Clang doesn't define a string hash function, except for Game Consoles.
+#define SB_HAS_STRING_HASH 0
+
+// Desktop Linux needs a using statement for the hash functions.
+#define SB_HAS_HASH_USING 0
+
+// Set this to 1 if hash functions for custom types can be defined as a
+// hash_value() function. Otherwise, they need to be placed inside a
+// partially-specified hash struct template with an operator().
+#define SB_HAS_HASH_VALUE 0
+
+// Set this to 1 if use of hash_map or hash_set causes a deprecation warning
+// (which then breaks the build).
+#define SB_HAS_HASH_WARNING 1
+
+// The location to include hash_map on this platform.
+#define SB_HASH_MAP_INCLUDE <ext/hash_map>
+
+// C++'s hash_map and hash_set are often found in different namespaces depending
+// on the compiler.
+#define SB_HASH_NAMESPACE __gnu_cxx
+
+// The location to include hash_set on this platform.
+#define SB_HASH_SET_INCLUDE <ext/hash_set>
+
+// Define this to how this platform copies varargs blocks.
+#define SB_VA_COPY(dest, source) va_copy(dest, source)
+
+// --- Filesystem Configuration ----------------------------------------------
+
+// The current platform's maximum length of the name of a single directory
+// entry, not including the absolute path.
+#define SB_FILE_MAX_NAME 64
+
+// The current platform's maximum length of an absolute path.
+#define SB_FILE_MAX_PATH 4096
+
+// The current platform's maximum number of files that can be opened at the
+// same time by one process.
+#define SB_FILE_MAX_OPEN 256
+
+// The current platform's file path component separator character. This is the
+// character that appears after a directory in a file path. For example, the
+// absolute canonical path of the file "/path/to/a/file.txt" uses '/' as a path
+// component separator character.
+#define SB_FILE_SEP_CHAR '/'
+
+// The current platform's alternate file path component separator character.
+// This is like SB_FILE_SEP_CHAR, except if your platform supports an alternate
+// character, then you can place that here. For example, on windows machines,
+// the primary separator character is probably '\', but the alternate is '/'.
+#define SB_FILE_ALT_SEP_CHAR '/'
+
+// The current platform's search path component separator character. When
+// specifying an ordered list of absolute paths of directories to search for a
+// given reason, this is the character that appears between entries. For
+// example, the search path of "/etc/search/first:/etc/search/second" uses ':'
+// as a search path component separator character.
+#define SB_PATH_SEP_CHAR ':'
+
+// The string form of SB_FILE_SEP_CHAR.
+#define SB_FILE_SEP_STRING "/"
+
+// The string form of SB_FILE_ALT_SEP_CHAR.
+#define SB_FILE_ALT_SEP_STRING "/"
+
+// The string form of SB_PATH_SEP_CHAR.
+#define SB_PATH_SEP_STRING ":"
+
+// --- Graphics Configuration ------------------------------------------------
+
+// Specifies whether this platform supports a performant accelerated blitter
+// API. The basic requirement is a scaled, clipped, alpha-blended blit.
+#define SB_HAS_BLITTER 0
+
+// Specifies the preferred byte order of color channels in a pixel. Refer to
+// starboard/configuration.h for the possible values. EGL/GLES platforms should
+// generally prefer a byte order of RGBA, regardless of endianness.
+#define SB_PREFERRED_RGBA_BYTE_ORDER SB_PREFERRED_RGBA_BYTE_ORDER_RGBA
+
+// Indicates whether or not the given platform supports bilinear filtering.
+// This can be checked to enable/disable renderer tests that verify that this is
+// working properly.
+#define SB_HAS_BILINEAR_FILTERING_SUPPORT 1
+
+// Indicates whether or not the given platform supports rendering of NV12
+// textures. These textures typically originate from video decoders.
+#define SB_HAS_NV12_TEXTURE_SUPPORT 1
+
+// Whether the current platform should frequently flip their display buffer.
+// If this is not required (e.g. SB_MUST_FREQUENTLY_FLIP_DISPLAY_BUFFER is set
+// to 0), then optimizations where the display buffer is not flipped if the
+// scene hasn't changed are enabled.
+#define SB_MUST_FREQUENTLY_FLIP_DISPLAY_BUFFER 0
+
+// --- I/O Configuration -----------------------------------------------------
+
+// Whether the current platform has microphone supported.
+#define SB_HAS_MICROPHONE 1
+
+// Whether the current platform implements the on screen keyboard interface.
+#define SB_HAS_ON_SCREEN_KEYBOARD 0
+
+// Whether the current platform has speech recognizer.
+#define SB_HAS_SPEECH_RECOGNIZER 1
+
+// Whether the current platform has speech synthesis.
+#define SB_HAS_SPEECH_SYNTHESIS 1
+
+// --- Media Configuration ---------------------------------------------------
+
+// Specifies whether this platform supports retrieving system-level closed
+// caption settings
+#define SB_HAS_CAPTIONS 1
+
+// Whether the current platform uses a media player that relies on a URL.
+#define SB_HAS_PLAYER_WITH_URL 0
+
+// The maximum audio bitrate the platform can decode.  The following value
+// equals to 5M bytes per seconds which is more than enough for compressed
+// audio.
+#define SB_MEDIA_MAX_AUDIO_BITRATE_IN_BITS_PER_SECOND (40 * 1024 * 1024)
+
+// The maximum video bitrate the platform can decode.  The following value
+// equals to 25M bytes per seconds which is more than enough for compressed
+// video.
+#define SB_MEDIA_MAX_VIDEO_BITRATE_IN_BITS_PER_SECOND (200 * 1024 * 1024)
+
+// Specifies whether this platform has webm/vp9 support.  This should be set to
+// non-zero on platforms with webm/vp9 support.
+#define SB_HAS_MEDIA_WEBM_VP9_SUPPORT 1
+
+// Specifies whether this platform updates audio frames asynchronously.  In such
+// case an extra parameter will be added to |SbAudioSinkConsumeFramesFunc| to
+// indicate the absolute time that the consumed audio frames are reported.
+// Check document for |SbAudioSinkConsumeFramesFunc| in audio_sink.h for more
+// details.
+#define SB_HAS_ASYNC_AUDIO_FRAMES_REPORTING 1
+
+// Specifies the stack size for threads created inside media stack.  Set to 0 to
+// use the default thread stack size.  Set to non-zero to explicitly set the
+// stack size for media stack threads.
+#define SB_MEDIA_THREAD_STACK_SIZE 0U
+
+// SbMediaTransferId argument |eotf| to SbMediaIsVideoSupported and remove the
+// function SbMediaIsTransferCharacteristicsSupported.
+#define SB_HAS_MEDIA_EOTF_CHECK_SUPPORT 1
+
+// --- Decoder-only Params ---
+
+// Specifies how media buffers must be aligned on this platform as some
+// decoders may have special requirement on the alignment of buffers being
+// decoded.
+#define SB_MEDIA_BUFFER_ALIGNMENT 128U
+
+// Specifies how video frame buffers must be aligned on this platform.
+#define SB_MEDIA_VIDEO_FRAME_ALIGNMENT 256U
+
+// The encoded video frames are compressed in different ways, their decoding
+// time can vary a lot.  Occasionally a single frame can take longer time to
+// decode than the average time per frame.  The player has to cache some frames
+// to account for such inconsistency.  The number of frames being cached are
+// controlled by the following two macros.
+//
+// Specify the number of video frames to be cached before the playback starts.
+// Note that set this value too large may increase the playback start delay.
+#define SB_MEDIA_MAXIMUM_VIDEO_PREROLL_FRAMES 4
+
+// Specify the number of video frames to be cached during playback.  A large
+// value leads to more stable fps but also causes the app to use more memory.
+#define SB_MEDIA_MAXIMUM_VIDEO_FRAMES 12
+
+// --- Memory Configuration --------------------------------------------------
+
+// The memory page size, which controls the size of chunks on memory that
+// allocators deal with, and the alignment of those chunks. This doesn't have to
+// be the hardware-defined physical page size, but it should be a multiple of
+// it.
+#define SB_MEMORY_PAGE_SIZE 4096
+
+// Whether this platform has and should use an MMAP function to map physical
+// memory to the virtual address space.
+#define SB_HAS_MMAP 1
+
+// Whether this platform can map executable memory. Implies SB_HAS_MMAP. This is
+// required for platforms that want to JIT.
+#define SB_CAN_MAP_EXECUTABLE_MEMORY 1
+
+// Whether this platform has and should use an growable heap (e.g. with sbrk())
+// to map physical memory to the virtual address space.
+#define SB_HAS_VIRTUAL_REGIONS 0
+
+// Specifies the alignment for IO Buffers, in bytes. Some low-level network APIs
+// may require buffers to have a specific alignment, and this is the place to
+// specify that.
+#define SB_NETWORK_IO_BUFFER_ALIGNMENT 16
+
+// Determines the alignment that allocations should have on this platform.
+#define SB_MALLOC_ALIGNMENT ((size_t)16U)
+
+// Determines the threshhold of allocation size that should be done with mmap
+// (if available), rather than allocated within the core heap.
+#define SB_DEFAULT_MMAP_THRESHOLD ((size_t)(256 * 1024U))
+
+// Defines the path where memory debugging logs should be written to.
+#define SB_MEMORY_LOG_PATH "/tmp/starboard"
+
+// --- Network Configuration -------------------------------------------------
+
+// Specifies whether this platform supports IPV6.
+#define SB_HAS_IPV6 1
+
+// Specifies whether this platform supports pipe.
+#define SB_HAS_PIPE 1
+
+// --- Timing API ------------------------------------------------------------
+
+// Whether this platform has an API to retrieve how long the current thread
+// has spent in the executing state.
+#define SB_HAS_TIME_THREAD_NOW 1
+
+// --- Thread Configuration --------------------------------------------------
+
+// Whether the current platform supports thread priorities.
+#define SB_HAS_THREAD_PRIORITY_SUPPORT 1
+
+// Defines the maximum number of simultaneous threads for this platform. Some
+// platforms require sharing thread handles with other kinds of system handles,
+// like mutexes, so we want to keep this managable.
+#define SB_MAX_THREADS 90
+
+// The maximum number of thread local storage keys supported by this platform.
+// This comes from bionic PTHREAD_KEYS_MAX in limits.h, which we've decided
+// to not include here to decrease symbol pollution.
+#define SB_MAX_THREAD_LOCAL_KEYS 128
+
+// The maximum length of the name for a thread, including the NULL-terminator.
+#define SB_MAX_THREAD_NAME_LENGTH 16
+
+// --- Tuneable Parameters ---------------------------------------------------
+
+// Specifies the network receive buffer size in bytes, set via
+// SbSocketSetReceiveBufferSize().
+//
+// Setting this to 0 indicates that SbSocketSetReceiveBufferSize() should
+// not be called. Use this for OSs (such as Linux) where receive buffer
+// auto-tuning is better.
+//
+// On some platforms, this may affect max TCP window size which may
+// dramatically affect throughput in the presence of latency.
+//
+// If your platform does not have a good TCP auto-tuning mechanism,
+// a setting of (128 * 1024) here is recommended.
+#define SB_NETWORK_RECEIVE_BUFFER_SIZE (0)
+
+// --- User Configuration ----------------------------------------------------
+
+// The maximum number of users that can be signed in at the same time.
+#define SB_USER_MAX_SIGNED_IN 1
+
+// --- Platform Specific Audits ----------------------------------------------
+
+#if !defined(__GNUC__)
+#error "Android builds need a GCC-like compiler (for the moment)."
+#endif
+
+#endif  // STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
diff --git a/src/starboard/android/shared/decode_target_create.cc b/src/starboard/android/shared/decode_target_create.cc
new file mode 100644
index 0000000..9b6f900
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_create.cc
@@ -0,0 +1,148 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_create.h"
+
+#include <android/native_window_jni.h>
+#include <jni.h>
+
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/decode_target.h"
+#include "starboard/shared/gles/gl_call.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+jobject CreateSurfaceTexture(int gl_texture_id) {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  jobject local_surface_texture = env->NewObjectOrAbort(
+      "android/graphics/SurfaceTexture", "(I)V", gl_texture_id);
+
+  jobject global_surface_texture =
+      env->ConvertLocalRefToGlobalRef(local_surface_texture);
+
+  return global_surface_texture;
+}
+
+jobject CreateSurfaceFromSurfaceTexture(jobject surface_texture) {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  jobject local_surface = env->NewObjectOrAbort(
+      "android/view/Surface", "(Landroid/graphics/SurfaceTexture;)V",
+      surface_texture);
+
+  jobject global_surface = env->ConvertLocalRefToGlobalRef(local_surface);
+
+  return global_surface;
+}
+
+struct CreateParams {
+  int width;
+  int height;
+  SbDecodeTargetFormat format;
+
+  SbDecodeTarget decode_target_out;
+};
+
+void CreateWithContextRunner(void* context) {
+  CreateParams* params = static_cast<CreateParams*>(context);
+
+  // Setup the GL texture that Android's MediaCodec library will target with
+  // the decoder.  We don't call glTexImage2d() on it, Android will handle
+  // the creation of the content when SurfaceTexture::updateTexImage() is
+  // called.
+  GLuint texture;
+  GL_CALL(glGenTextures(1, &texture));
+  GL_CALL(glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture));
+  GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER,
+                          GL_LINEAR));
+  GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER,
+                          GL_LINEAR));
+  GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S,
+                          GL_CLAMP_TO_EDGE));
+  GL_CALL(glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T,
+                          GL_CLAMP_TO_EDGE));
+
+  SbDecodeTarget decode_target = new SbDecodeTargetPrivate;
+  decode_target->data = new SbDecodeTargetPrivate::Data;
+
+  // Wrap the GL texture in an Android SurfaceTexture object.
+  decode_target->data->surface_texture = CreateSurfaceTexture(texture);
+
+  // We will also need an Android Surface object in order to obtain a
+  // ANativeWindow object that we can pass into the AMediaCodec library.
+  decode_target->data->surface =
+      CreateSurfaceFromSurfaceTexture(decode_target->data->surface_texture);
+
+  decode_target->data->native_window =
+      ANativeWindow_fromSurface(JniEnvExt::Get(), decode_target->data->surface);
+
+  // Setup our publicly accessible decode target information.
+  decode_target->data->info.format = params->format;
+  decode_target->data->info.is_opaque = true;
+  decode_target->data->info.width = params->width;
+  decode_target->data->info.height = params->height;
+  decode_target->data->info.planes[0].texture = texture;
+  decode_target->data->info.planes[0].gl_texture_target =
+      GL_TEXTURE_EXTERNAL_OES;
+  decode_target->data->info.planes[0].width = params->width;
+  decode_target->data->info.planes[0].height = params->height;
+
+  // These values will be initialized when SbPlayerGetCurrentFrame() is called.
+  decode_target->data->info.planes[0].content_region.left = 0;
+  decode_target->data->info.planes[0].content_region.right = 0;
+  decode_target->data->info.planes[0].content_region.top = 0;
+  decode_target->data->info.planes[0].content_region.bottom = 0;
+
+  GL_CALL(glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0));
+
+  params->decode_target_out = decode_target;
+}
+
+}  // namespace
+
+SbDecodeTarget DecodeTargetCreate(
+    SbDecodeTargetGraphicsContextProvider* provider,
+    SbDecodeTargetFormat format,
+    int width,
+    int height) {
+  SB_DCHECK(format == kSbDecodeTargetFormat1PlaneRGBA);
+  if (format != kSbDecodeTargetFormat1PlaneRGBA) {
+    return kSbDecodeTargetInvalid;
+  }
+
+  CreateParams params;
+  params.width = width;
+  params.height = height;
+  params.format = format;
+  params.decode_target_out = kSbDecodeTargetInvalid;
+
+  SbDecodeTargetRunInGlesContext(
+      provider, &CreateWithContextRunner, &params);
+  return params.decode_target_out;
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/decode_target_create.h b/src/starboard/android/shared/decode_target_create.h
new file mode 100644
index 0000000..3f32621
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_create.h
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
+#define STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
+
+#include "starboard/decode_target.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+SbDecodeTarget DecodeTargetCreate(
+    SbDecodeTargetGraphicsContextProvider* provider,
+    SbDecodeTargetFormat format,
+    int width,
+    int height);
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_DECODE_TARGET_CREATE_H_
diff --git a/src/starboard/android/shared/decode_target_get_info.cc b/src/starboard/android/shared/decode_target_get_info.cc
new file mode 100644
index 0000000..38ded1a
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_get_info.cc
@@ -0,0 +1,29 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/decode_target.h"
+#include "starboard/memory.h"
+
+bool SbDecodeTargetGetInfo(SbDecodeTarget decode_target,
+                           SbDecodeTargetInfo* out_info) {
+  if (!SbMemoryIsZero(out_info, sizeof(*out_info))) {
+    SB_DCHECK(false) << "out_info must be zeroed out.";
+    return false;
+  }
+
+  SbMemoryCopy(out_info, &decode_target->data->info, sizeof(*out_info));
+
+  return true;
+}
diff --git a/src/starboard/android/shared/decode_target_internal.cc b/src/starboard/android/shared/decode_target_internal.cc
new file mode 100644
index 0000000..957fb1d
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_internal.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/decode_target_internal.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+SbDecodeTargetPrivate::Data::~Data() {
+  ANativeWindow_release(native_window);
+
+  JniEnvExt* env = JniEnvExt::Get();
+  env->DeleteGlobalRef(surface);
+  env->DeleteGlobalRef(surface_texture);
+
+  glDeleteTextures(1, &info.planes[0].texture);
+  SB_DCHECK(glGetError() == GL_NO_ERROR);
+}
diff --git a/src/starboard/android/shared/decode_target_internal.h b/src/starboard/android/shared/decode_target_internal.h
new file mode 100644
index 0000000..953840a
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_internal.h
@@ -0,0 +1,47 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
+
+#include <android/native_window.h>
+#include <GLES2/gl2.h>
+#include <jni.h>
+
+#include "starboard/common/ref_counted.h"
+#include "starboard/decode_target.h"
+
+struct SbDecodeTargetPrivate {
+  class Data : public starboard::RefCounted<Data> {
+   public:
+    Data() {}
+
+    // Java objects which wrap the texture.  We hold on to global references
+    // to these objects.
+    jobject surface_texture;
+    jobject surface;
+    ANativeWindow* native_window;
+
+    // Publicly accessible information about the decode target.
+    SbDecodeTargetInfo info;
+
+   private:
+    friend class starboard::RefCounted<Data>;
+    ~Data();
+  };
+
+  starboard::scoped_refptr<Data> data;
+};
+
+#endif  // STARBOARD_ANDROID_SHARED_DECODE_TARGET_INTERNAL_H_
diff --git a/src/starboard/android/shared/decode_target_release.cc b/src/starboard/android/shared/decode_target_release.cc
new file mode 100644
index 0000000..29a3f4c
--- /dev/null
+++ b/src/starboard/android/shared/decode_target_release.cc
@@ -0,0 +1,28 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <GLES2/gl2.h>
+
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/decode_target.h"
+#include "starboard/log.h"
+
+void SbDecodeTargetRelease(SbDecodeTarget decode_target) {
+  // Most of the actual data within |decode_target| is stored in the reference
+  // counted decode_target->data, so deleting |decode_target| here may not
+  // actually release any resources, if there are other references to
+  // decode_target->data.
+  delete decode_target;
+}
diff --git a/src/starboard/android/shared/directory_close.cc b/src/starboard/android/shared/directory_close.cc
new file mode 100644
index 0000000..30d52ec
--- /dev/null
+++ b/src/starboard/android/shared/directory_close.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_close.h"
+
+bool SbDirectoryClose(SbDirectory directory) {
+  if (directory && directory->asset_dir) {
+    AAssetDir_close(directory->asset_dir);
+    delete directory;
+    return true;
+  }
+
+  return ::starboard::shared::iso::impl::SbDirectoryClose(directory);
+}
diff --git a/src/starboard/android/shared/directory_get_next.cc b/src/starboard/android/shared/directory_get_next.cc
new file mode 100644
index 0000000..f1e5b02
--- /dev/null
+++ b/src/starboard/android/shared/directory_get_next.cc
@@ -0,0 +1,35 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_get_next.h"
+
+bool SbDirectoryGetNext(SbDirectory directory, SbDirectoryEntry* out_entry) {
+  if (directory && directory->asset_dir && out_entry) {
+    const char* file_name = AAssetDir_getNextFileName(directory->asset_dir);
+    if (file_name == NULL) {
+      return false;
+    }
+    size_t size = SB_ARRAY_SIZE_INT(out_entry->name);
+    SbStringCopy(out_entry->name, file_name, size);
+    return true;
+  }
+
+  return ::starboard::shared::iso::impl::SbDirectoryGetNext(directory,
+                                                            out_entry);
+}
diff --git a/src/starboard/android/shared/directory_internal.h b/src/starboard/android/shared/directory_internal.h
new file mode 100644
index 0000000..1e2108b
--- /dev/null
+++ b/src/starboard/android/shared/directory_internal.h
@@ -0,0 +1,37 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
+
+#include <android/asset_manager.h>
+
+#include <dirent.h>
+
+#include "starboard/directory.h"
+#include "starboard/shared/internal_only.h"
+
+struct SbDirectoryPrivate {
+  // Note: Only one of these two fields will be valid for any given file.
+
+  // The ISO C directory stream handle, or NULL if it's an asset directory.
+  DIR* directory;
+
+  // If not NULL this is an Android asset directory.
+  AAssetDir* asset_dir;
+
+  SbDirectoryPrivate() : directory(NULL), asset_dir(NULL) {}
+};
+
+#endif  // STARBOARD_ANDROID_SHARED_DIRECTORY_INTERNAL_H_
diff --git a/src/starboard/android/shared/directory_open.cc b/src/starboard/android/shared/directory_open.cc
new file mode 100644
index 0000000..9c0f7a8
--- /dev/null
+++ b/src/starboard/android/shared/directory_open.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/directory.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/shared/iso/impl/directory_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAssetDir;
+
+SbDirectory SbDirectoryOpen(const char* path, SbFileError* out_error) {
+  if (!IsAndroidAssetPath(path)) {
+    return ::starboard::shared::iso::impl::SbDirectoryOpen(path, out_error);
+  }
+
+  AAssetDir* asset_dir = OpenAndroidAssetDir(path);
+  if (asset_dir) {
+    SbDirectory result = new SbDirectoryPrivate();
+    result->asset_dir = asset_dir;
+    return result;
+  }
+
+  if (out_error) {
+    *out_error = kSbFileErrorFailed;
+  }
+  return kSbDirectoryInvalid;
+}
diff --git a/src/starboard/android/shared/drm_create_system.cc b/src/starboard/android/shared/drm_create_system.cc
new file mode 100644
index 0000000..e92d6a1
--- /dev/null
+++ b/src/starboard/android/shared/drm_create_system.cc
@@ -0,0 +1,48 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/drm_system.h"
+
+#include "starboard/android/shared/media_common.h"
+
+SbDrmSystem SbDrmCreateSystem(
+    const char* key_system,
+    void* context,
+    SbDrmSessionUpdateRequestFunc update_request_callback,
+    SbDrmSessionUpdatedFunc session_updated_callback,
+    SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback,
+    SbDrmServerCertificateUpdatedFunc server_certificate_updated_callback,
+    SbDrmSessionClosedFunc session_closed_callback) {
+  using starboard::android::shared::DrmSystem;
+  using starboard::android::shared::IsWidevine;
+
+  if (!update_request_callback || !session_updated_callback ||
+      !key_statuses_changed_callback || !server_certificate_updated_callback ||
+      !session_closed_callback) {
+    return kSbDrmSystemInvalid;
+  }
+
+  if (!IsWidevine(key_system)) {
+    return kSbDrmSystemInvalid;
+  }
+
+  DrmSystem* drm_system =
+      new DrmSystem(context, update_request_callback, session_updated_callback,
+                    key_statuses_changed_callback);
+  if (!drm_system->is_valid()) {
+    delete drm_system;
+    return kSbDrmSystemInvalid;
+  }
+  return drm_system;
+}
diff --git a/src/starboard/android/shared/drm_system.cc b/src/starboard/android/shared/drm_system.cc
new file mode 100644
index 0000000..e01c8f7
--- /dev/null
+++ b/src/starboard/android/shared/drm_system.cc
@@ -0,0 +1,307 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/drm_system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+
+namespace {
+
+using starboard::android::shared::DrmSystem;
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+const char kNoUrl[] = "";
+
+// Using all capital names to be consistent with other Android media statuses.
+// They are defined in the same order as in their Java counterparts.  Their
+// values should be kept in consistent with their Java counterparts defined in
+// android.media.MediaDrm.KeyStatus.
+const jint MEDIA_DRM_KEY_STATUS_EXPIRED = 1;
+const jint MEDIA_DRM_KEY_STATUS_INTERNAL_ERROR = 4;
+const jint MEDIA_DRM_KEY_STATUS_OUTPUT_NOT_ALLOWED = 2;
+const jint MEDIA_DRM_KEY_STATUS_PENDING = 3;
+const jint MEDIA_DRM_KEY_STATUS_USABLE = 0;
+
+}  // namespace
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_MediaDrmBridge_nativeOnSessionMessage(
+    JNIEnv* env,
+    jobject unused_this,
+    jlong native_media_drm_bridge,
+    jint ticket,
+    jbyteArray j_session_id,
+    jint request_type,
+    jbyteArray j_message) {
+  jbyte* session_id_elements = env->GetByteArrayElements(j_session_id, NULL);
+  jsize session_id_size = env->GetArrayLength(j_session_id);
+
+  jbyte* message_elements = env->GetByteArrayElements(j_message, NULL);
+  jsize message_size = env->GetArrayLength(j_message);
+
+  SB_DCHECK(session_id_elements);
+  SB_DCHECK(message_elements);
+
+  DrmSystem* drm_system = reinterpret_cast<DrmSystem*>(native_media_drm_bridge);
+  SB_DCHECK(drm_system);
+  drm_system->CallUpdateRequestCallback(ticket, session_id_elements,
+                                        session_id_size, message_elements,
+                                        message_size, kNoUrl);
+  env->ReleaseByteArrayElements(j_session_id, session_id_elements, JNI_ABORT);
+  env->ReleaseByteArrayElements(j_message, message_elements, JNI_ABORT);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_MediaDrmBridge_nativeOnKeyStatusChange(
+    JniEnvExt* env,
+    jobject unused_this,
+    jlong native_media_drm_bridge,
+    jbyteArray j_session_id,
+    jobjectArray j_key_status_array) {
+  jbyte* session_id_elements = env->GetByteArrayElements(j_session_id, NULL);
+  jsize session_id_size = env->GetArrayLength(j_session_id);
+
+  SB_DCHECK(session_id_elements);
+
+  // NULL array indicates key status isn't supported (i.e. Android API < 23)
+  jsize length = (j_key_status_array == NULL) ? 0
+      : env->GetArrayLength(j_key_status_array);
+  std::vector<SbDrmKeyId> drm_key_ids(length);
+  std::vector<SbDrmKeyStatus> drm_key_statuses(length);
+
+  for (jsize i = 0; i < length; ++i) {
+    jobject j_key_status =
+        env->GetObjectArrayElementOrAbort(j_key_status_array, i);
+    jbyteArray j_key_id = static_cast<jbyteArray>(
+        env->CallObjectMethodOrAbort(j_key_status, "getKeyId", "()[B"));
+
+    jbyte* key_id_elements = env->GetByteArrayElements(j_key_id, NULL);
+    jsize key_id_size = env->GetArrayLength(j_key_id);
+    SB_DCHECK(key_id_elements);
+
+    SB_DCHECK(key_id_size <= sizeof(drm_key_ids[i].identifier));
+    SbMemoryCopy(drm_key_ids[i].identifier, key_id_elements, key_id_size);
+    env->ReleaseByteArrayElements(j_key_id, key_id_elements, JNI_ABORT);
+    drm_key_ids[i].identifier_size = key_id_size;
+
+    jint j_status_code =
+        env->CallIntMethodOrAbort(j_key_status, "getStatusCode", "()I");
+    if (j_status_code == MEDIA_DRM_KEY_STATUS_EXPIRED) {
+      drm_key_statuses[i] = kSbDrmKeyStatusExpired;
+    } else if (j_status_code == MEDIA_DRM_KEY_STATUS_INTERNAL_ERROR) {
+      drm_key_statuses[i] = kSbDrmKeyStatusError;
+    } else if (j_status_code == MEDIA_DRM_KEY_STATUS_OUTPUT_NOT_ALLOWED) {
+      drm_key_statuses[i] = kSbDrmKeyStatusRestricted;
+    } else if (j_status_code == MEDIA_DRM_KEY_STATUS_PENDING) {
+      drm_key_statuses[i] = kSbDrmKeyStatusPending;
+    } else if (j_status_code == MEDIA_DRM_KEY_STATUS_USABLE) {
+      drm_key_statuses[i] = kSbDrmKeyStatusUsable;
+    } else {
+      SB_NOTREACHED();
+      drm_key_statuses[i] = kSbDrmKeyStatusError;
+    }
+  }
+
+  DrmSystem* drm_system = reinterpret_cast<DrmSystem*>(native_media_drm_bridge);
+  SB_DCHECK(drm_system);
+  drm_system->CallDrmSessionKeyStatusesChangedCallback(
+      session_id_elements, session_id_size, drm_key_ids, drm_key_statuses);
+
+  env->ReleaseByteArrayElements(j_session_id, session_id_elements, JNI_ABORT);
+}
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+jbyteArray ByteArrayFromRaw(const void* data, int size) {
+  return JniEnvExt::Get()->NewByteArrayFromRaw(static_cast<const jbyte*>(data),
+                                               size);
+}
+
+}  // namespace
+
+DrmSystem::DrmSystem(
+    void* context,
+    SbDrmSessionUpdateRequestFunc update_request_callback,
+    SbDrmSessionUpdatedFunc session_updated_callback,
+    SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback)
+    : context_(context),
+      update_request_callback_(update_request_callback),
+      session_updated_callback_(session_updated_callback),
+      key_statuses_changed_callback_(key_statuses_changed_callback),
+      j_media_drm_bridge_(NULL),
+      j_media_crypto_(NULL),
+      hdcp_lost_(false) {
+  JniEnvExt* env = JniEnvExt::Get();
+  j_media_drm_bridge_ = env->CallStaticObjectMethodOrAbort(
+      "dev/cobalt/media/MediaDrmBridge", "create",
+      "(J)Ldev/cobalt/media/MediaDrmBridge;", reinterpret_cast<jlong>(this));
+  if (!j_media_drm_bridge_) {
+    SB_LOG(ERROR) << "Failed to create MediaDrmBridge.";
+    return;
+  }
+  j_media_drm_bridge_ = env->ConvertLocalRefToGlobalRef(j_media_drm_bridge_);
+  j_media_crypto_ = env->CallObjectMethodOrAbort(
+      j_media_drm_bridge_, "getMediaCrypto", "()Landroid/media/MediaCrypto;");
+  if (!j_media_crypto_) {
+    SB_LOG(ERROR) << "Failed to create MediaCrypto.";
+    return;
+  }
+  j_media_crypto_ = env->ConvertLocalRefToGlobalRef(j_media_crypto_);
+}
+
+DrmSystem::~DrmSystem() {
+  JniEnvExt* env = JniEnvExt::Get();
+  if (j_media_crypto_) {
+    env->DeleteGlobalRef(j_media_crypto_);
+    j_media_crypto_ = NULL;
+  }
+  if (j_media_drm_bridge_) {
+    env->CallVoidMethodOrAbort(j_media_drm_bridge_, "destroy", "()V");
+    env->DeleteGlobalRef(j_media_drm_bridge_);
+    j_media_drm_bridge_ = NULL;
+  }
+}
+
+void DrmSystem::GenerateSessionUpdateRequest(int ticket,
+                                             const char* type,
+                                             const void* initialization_data,
+                                             int initialization_data_size) {
+  ScopedLocalJavaRef<jbyteArray> j_init_data(
+      ByteArrayFromRaw(initialization_data, initialization_data_size));
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(type));
+  env->CallVoidMethodOrAbort(
+      j_media_drm_bridge_, "createSession", "(I[BLjava/lang/String;)V",
+      static_cast<jint>(ticket), j_init_data.Get(), j_mime.Get());
+  // |update_request_callback_| will be called by Java calling into
+  // |onSessionMessage|.
+}
+
+void DrmSystem::UpdateSession(int ticket,
+                              const void* key,
+                              int key_size,
+                              const void* session_id,
+                              int session_id_size) {
+  ScopedLocalJavaRef<jbyteArray> j_session_id(
+      ByteArrayFromRaw(session_id, session_id_size));
+  ScopedLocalJavaRef<jbyteArray> j_response(ByteArrayFromRaw(key, key_size));
+
+  jboolean status = JniEnvExt::Get()->CallBooleanMethodOrAbort(
+      j_media_drm_bridge_, "updateSession", "([B[B)Z", j_session_id.Get(),
+      j_response.Get());
+  session_updated_callback_(
+      this, context_, ticket,
+      status == JNI_TRUE ? kSbDrmStatusSuccess : kSbDrmStatusUnknownError, NULL,
+      session_id, session_id_size);
+}
+
+void DrmSystem::CloseSession(const void* session_id, int session_id_size) {
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jbyteArray> j_session_id(
+      ByteArrayFromRaw(session_id, session_id_size));
+  std::string session_id_as_string(
+      static_cast<const char*>(session_id),
+      static_cast<const char*>(session_id) + session_id_size);
+
+  {
+    ScopedLock scoped_lock(mutex_);
+    auto iter = cached_drm_key_ids_.find(session_id_as_string);
+    if (iter != cached_drm_key_ids_.end()) {
+      cached_drm_key_ids_.erase(iter);
+    }
+  }
+  env->CallVoidMethodOrAbort(j_media_drm_bridge_, "closeSession", "([B)V",
+                             j_session_id.Get());
+}
+
+DrmSystem::DecryptStatus DrmSystem::Decrypt(InputBuffer* buffer) {
+  SB_DCHECK(buffer);
+  SB_DCHECK(buffer->drm_info());
+  SB_DCHECK(j_media_crypto_);
+  // The actual decryption will take place by calling |queueSecureInputBuffer|
+  // in the decoders.  Our existence implies that there is enough information
+  // to perform the decryption.
+  // TODO: Returns kRetry when |UpdateSession| is not called at all to allow the
+  //       player worker to handle the retry logic.
+  return kSuccess;
+}
+
+void DrmSystem::CallUpdateRequestCallback(int ticket,
+                                          const void* session_id,
+                                          int session_id_size,
+                                          const void* content,
+                                          int content_size,
+                                          const char* url) {
+  update_request_callback_(this, context_, ticket, kSbDrmStatusSuccess,
+                           kSbDrmSessionRequestTypeLicenseRequest, NULL,
+                           session_id, session_id_size, content, content_size,
+                           url);
+}
+
+void DrmSystem::CallDrmSessionKeyStatusesChangedCallback(
+    const void* session_id,
+    int session_id_size,
+    const std::vector<SbDrmKeyId>& drm_key_ids,
+    const std::vector<SbDrmKeyStatus>& drm_key_statuses) {
+  SB_DCHECK(drm_key_ids.size() == drm_key_statuses.size());
+
+  std::string session_id_as_string(
+      static_cast<const char*>(session_id),
+      static_cast<const char*>(session_id) + session_id_size);
+
+  bool hdcp_lost = false;
+  {
+    ScopedLock scoped_lock(mutex_);
+    cached_drm_key_ids_[session_id_as_string] = drm_key_ids;
+    hdcp_lost = hdcp_lost_;
+  }
+
+  if (hdcp_lost) {
+    OnInsufficientOutputProtection();
+    return;
+  }
+
+  key_statuses_changed_callback_(this, context_, session_id, session_id_size,
+                                 static_cast<int>(drm_key_ids.size()),
+                                 drm_key_ids.data(), drm_key_statuses.data());
+}
+
+void DrmSystem::OnInsufficientOutputProtection() {
+  // HDCP has lost, update the statuses of all keys in all known sessions to be
+  // restricted.
+  ScopedLock scoped_lock(mutex_);
+  hdcp_lost_ = true;
+  for (auto& iter : cached_drm_key_ids_) {
+    const std::string& session_id = iter.first;
+    const std::vector<SbDrmKeyId>& drm_key_ids = iter.second;
+    std::vector<SbDrmKeyStatus> drm_key_statuses(drm_key_ids.size(),
+                                                 kSbDrmKeyStatusRestricted);
+
+    key_statuses_changed_callback_(this, context_, session_id.data(),
+                                   session_id.size(),
+                                   static_cast<int>(drm_key_ids.size()),
+                                   drm_key_ids.data(), drm_key_statuses.data());
+  }
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/drm_system.h b/src/starboard/android/shared/drm_system.h
new file mode 100644
index 0000000..cec052b
--- /dev/null
+++ b/src/starboard/android/shared/drm_system.h
@@ -0,0 +1,99 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
+#define STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
+
+#include "starboard/shared/starboard/drm/drm_system_internal.h"
+
+#include <jni.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class DrmSystem : public ::SbDrmSystemPrivate {
+ public:
+  DrmSystem(void* context,
+            SbDrmSessionUpdateRequestFunc update_request_callback,
+            SbDrmSessionUpdatedFunc session_updated_callback,
+            SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback);
+
+  ~DrmSystem() override;
+  void GenerateSessionUpdateRequest(int ticket,
+                                    const char* type,
+                                    const void* initialization_data,
+                                    int initialization_data_size) override;
+  void UpdateSession(int ticket,
+                     const void* key,
+                     int key_size,
+                     const void* session_id,
+                     int session_id_size);
+  void CloseSession(const void* session_id, int session_id_size) override;
+  DecryptStatus Decrypt(InputBuffer* buffer) override;
+#if SB_API_VERSION >= 10
+  void UpdateServerCertificate(int ticket,
+                               const void* certificate,
+                               int certificate_size) override {
+    SB_UNREFERENCED_PARAMETER(ticket);
+    SB_UNREFERENCED_PARAMETER(certificate);
+    SB_UNREFERENCED_PARAMETER(certificate_size);
+  }
+#endif  // SB_API_VERSION >= 10
+
+  jobject GetMediaCrypto() const { return j_media_crypto_; }
+  void CallUpdateRequestCallback(int ticket,
+                                 const void* session_id,
+                                 int session_id_size,
+                                 const void* content,
+                                 int content_size,
+                                 const char* url);
+  void CallDrmSessionKeyStatusesChangedCallback(
+      const void* session_id,
+      int session_id_size,
+      const std::vector<SbDrmKeyId>& drm_key_ids,
+      const std::vector<SbDrmKeyStatus>& drm_key_statuses);
+  void OnInsufficientOutputProtection();
+
+  bool is_valid() const {
+    return j_media_drm_bridge_ != NULL && j_media_crypto_ != NULL;
+  }
+
+ private:
+  void* context_;
+  SbDrmSessionUpdateRequestFunc update_request_callback_;
+  SbDrmSessionUpdatedFunc session_updated_callback_;
+  // TODO: Update key statuses to Cobalt.
+  SbDrmSessionKeyStatusesChangedFunc key_statuses_changed_callback_;
+
+  jobject j_media_drm_bridge_;
+  jobject j_media_crypto_;
+
+  Mutex mutex_;
+  std::unordered_map<std::string, std::vector<SbDrmKeyId> > cached_drm_key_ids_;
+  bool hdcp_lost_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_DRM_SYSTEM_H_
diff --git a/src/starboard/android/shared/egl_swap_buffers.cc b/src/starboard/android/shared/egl_swap_buffers.cc
new file mode 100644
index 0000000..e1567f8
--- /dev/null
+++ b/src/starboard/android/shared/egl_swap_buffers.cc
@@ -0,0 +1,44 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+
+#include "starboard/android/shared/video_window.h"
+#include "starboard/shared/gles/gl_call.h"
+
+extern "C" {
+EGLBoolean __real_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
+
+// This needs to be exported to ensure shared_library targets include it.
+SB_EXPORT_PLATFORM EGLBoolean __wrap_eglSwapBuffers(
+    EGLDisplay dpy, EGLSurface surface) {
+  // Kick off the GPU while waiting for new player bounds to take effect.
+  GL_CALL(glFlush());
+
+  // Wait for player bounds to take effect before presenting the UI frame which
+  // uses those player bounds. This helps to keep punch-out videos in sync with
+  // the UI frame.
+  // TODO: It is possible for the new UI frame to be displayed too late
+  // (especially if there's a lot to render), so this case still needs to
+  // be handled.
+
+  // Note, we're no longer calling WaitForVideoBoundsUpdate because it does
+  // not work properly without calling SurfaceHolder setFixedSize.
+  // starboard::android::shared::WaitForVideoBoundsUpdate();
+
+  return __real_eglSwapBuffers(dpy, surface);
+}
+
+}
diff --git a/src/starboard/android/shared/file_can_open.cc b/src/starboard/android/shared/file_can_open.cc
new file mode 100644
index 0000000..5aeb5fd
--- /dev/null
+++ b/src/starboard/android/shared/file_can_open.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/directory.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_can_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+bool SbFileCanOpen(const char* path, int flags) {
+  if (!IsAndroidAssetPath(path)) {
+    return ::starboard::shared::posix::impl::FileCanOpen(path, flags);
+  }
+
+  SbFile file = SbFileOpen(path, flags | kSbFileOpenOnly, NULL, NULL);
+  bool result = SbFileIsValid(file);
+  SbFileClose(file);
+
+  if (!result) {
+    SbDirectory directory = SbDirectoryOpen(path, NULL);
+    result = SbDirectoryIsValid(directory);
+    SbDirectoryClose(directory);
+  }
+
+  return result;
+}
diff --git a/src/starboard/android/shared/file_close.cc b/src/starboard/android/shared/file_close.cc
new file mode 100644
index 0000000..192aede
--- /dev/null
+++ b/src/starboard/android/shared/file_close.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_close.h"
+
+bool SbFileClose(SbFile file) {
+  if (file && file->asset) {
+    AAsset_close(file->asset);
+    delete file;
+    return true;
+  }
+
+  return ::starboard::shared::posix::impl::FileClose(file);
+}
diff --git a/src/starboard/android/shared/file_delete.cc b/src/starboard/android/shared/file_delete.cc
new file mode 100644
index 0000000..9a0ab8b
--- /dev/null
+++ b/src/starboard/android/shared/file_delete.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_delete.h"
+
+bool SbFileDelete(const char* path) {
+  return ::starboard::shared::posix::impl::FileDelete(path);
+}
diff --git a/src/starboard/android/shared/file_exists.cc b/src/starboard/android/shared/file_exists.cc
new file mode 100644
index 0000000..cfe253c
--- /dev/null
+++ b/src/starboard/android/shared/file_exists.cc
@@ -0,0 +1,19 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+bool SbFileExists(const char* path) {
+  return SbFileCanOpen(path, kSbFileRead);
+}
diff --git a/src/starboard/android/shared/file_flush.cc b/src/starboard/android/shared/file_flush.cc
new file mode 100644
index 0000000..25634ca
--- /dev/null
+++ b/src/starboard/android/shared/file_flush.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_flush.h"
+
+bool SbFileFlush(SbFile file) {
+  return ::starboard::shared::posix::impl::FileFlush(file);
+}
diff --git a/src/starboard/android/shared/file_get_info.cc b/src/starboard/android/shared/file_get_info.cc
new file mode 100644
index 0000000..d7e7559
--- /dev/null
+++ b/src/starboard/android/shared/file_get_info.cc
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_get_info.h"
+
+bool SbFileGetInfo(SbFile file, SbFileInfo* out_info) {
+  if (file && file->asset && out_info) {
+    out_info->creation_time = 0;
+    out_info->is_directory = 0;
+    out_info->is_symbolic_link = 0;
+    out_info->last_accessed = 0;
+    out_info->last_modified = 0;
+    out_info->size = AAsset_getLength(file->asset);
+    return true;
+  }
+
+  return ::starboard::shared::posix::impl::FileGetInfo(file, out_info);
+}
diff --git a/src/starboard/android/shared/file_get_path_info.cc b/src/starboard/android/shared/file_get_path_info.cc
new file mode 100644
index 0000000..9bc05dc
--- /dev/null
+++ b/src/starboard/android/shared/file_get_path_info.cc
@@ -0,0 +1,51 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/directory.h"
+
+#include "starboard/android/shared/directory_internal.h"
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_get_path_info.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+bool SbFileGetPathInfo(const char* path, SbFileInfo* out_info) {
+  if (!IsAndroidAssetPath(path)) {
+    return ::starboard::shared::posix::impl::FileGetPathInfo(path, out_info);
+  }
+
+  SbFile file = SbFileOpen(path, kSbFileRead, NULL, NULL);
+  if (file) {
+    bool result = SbFileGetInfo(file, out_info);
+    SbFileClose(file);
+    return result;
+  }
+
+  SbDirectory directory = SbDirectoryOpen(path, NULL);
+  if (directory && directory->asset_dir) {
+    out_info->creation_time = 0;
+    out_info->is_directory = 1;
+    out_info->is_symbolic_link = 0;
+    out_info->last_accessed = 0;
+    out_info->last_modified = 0;
+    out_info->size = 0;
+    SbDirectoryClose(directory);
+    return true;
+  }
+
+  return false;
+}
diff --git a/src/starboard/android/shared/file_internal.cc b/src/starboard/android/shared/file_internal.cc
new file mode 100644
index 0000000..fd7ae03
--- /dev/null
+++ b/src/starboard/android/shared/file_internal.cc
@@ -0,0 +1,138 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/file_internal.h"
+
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/log.h>
+#include <jni.h>
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+const char* g_app_assets_dir = "/cobalt/assets";
+const char* g_app_files_dir = NULL;
+const char* g_app_cache_dir = NULL;
+const char* g_app_lib_dir = NULL;
+
+namespace {
+jobject g_java_asset_manager;
+AAssetManager* g_asset_manager;
+
+// Copies the characters from a jstring and returns a newly allocated buffer
+// with the result.
+const char* DuplicateJavaString(JniEnvExt* env, jstring j_string) {
+  SB_DCHECK(j_string);
+  std::string utf_str = env->GetStringStandardUTFOrAbort(j_string);
+  const char* result = SbStringDuplicate(utf_str.c_str());
+  return result;
+}
+
+}  // namespace
+
+void SbFileAndroidInitialize() {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  SB_DCHECK(g_java_asset_manager == NULL);
+  SB_DCHECK(g_asset_manager == NULL);
+  ScopedLocalJavaRef<jstring> j_app(
+      env->CallStarboardObjectMethodOrAbort(
+          "getApplicationContext", "()Landroid/content/Context;"));
+  g_java_asset_manager = env->ConvertLocalRefToGlobalRef(
+      env->CallObjectMethodOrAbort(j_app.Get(),
+          "getAssets", "()Landroid/content/res/AssetManager;"));
+  g_asset_manager = AAssetManager_fromJava(env, g_java_asset_manager);
+
+  SB_DCHECK(g_app_files_dir == NULL);
+  ScopedLocalJavaRef<jstring> j_string(
+      env->CallStarboardObjectMethodOrAbort("getFilesAbsolutePath",
+                                           "()Ljava/lang/String;"));
+  g_app_files_dir = DuplicateJavaString(env, j_string.Get());
+  SB_DLOG(INFO) << "Files dir: " << g_app_files_dir;
+
+  SB_DCHECK(g_app_cache_dir == NULL);
+  j_string.Reset(
+      env->CallStarboardObjectMethodOrAbort("getCacheAbsolutePath",
+                                           "()Ljava/lang/String;"));
+  g_app_cache_dir = DuplicateJavaString(env, j_string.Get());
+  SB_DLOG(INFO) << "Cache dir: " << g_app_cache_dir;
+
+  SB_DCHECK(g_app_lib_dir == NULL);
+  ScopedLocalJavaRef<jobject> j_app_info(
+      env->CallObjectMethodOrAbort(j_app.Get(), "getApplicationInfo",
+                                   "()Landroid/content/pm/ApplicationInfo;"));
+  j_string.Reset(env->GetStringFieldOrAbort(j_app_info.Get(),
+                                            "nativeLibraryDir"));
+  g_app_lib_dir = DuplicateJavaString(env, j_string.Get());
+  SB_DLOG(INFO) << "Lib dir: " << g_app_lib_dir;
+}
+
+void SbFileAndroidTeardown() {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  if (g_java_asset_manager) {
+    env->DeleteGlobalRef(g_java_asset_manager);
+    g_java_asset_manager = NULL;
+    g_asset_manager = NULL;
+  }
+
+  if (g_app_files_dir) {
+    SbMemoryDeallocate(const_cast<char*>(g_app_files_dir));
+    g_app_files_dir = NULL;
+  }
+
+  if (g_app_cache_dir) {
+    SbMemoryDeallocate(const_cast<char*>(g_app_cache_dir));
+    g_app_cache_dir = NULL;
+  }
+}
+
+bool IsAndroidAssetPath(const char* path) {
+  size_t prefix_len = SbStringGetLength(g_app_assets_dir);
+  return path != NULL
+      && SbStringCompare(g_app_assets_dir, path, prefix_len) == 0
+      && (path[prefix_len] == '/' || path[prefix_len] == '\0');
+}
+
+AAsset* OpenAndroidAsset(const char* path) {
+  if (!IsAndroidAssetPath(path) || g_asset_manager == NULL) {
+    return NULL;
+  }
+  const char* asset_path = path + SbStringGetLength(g_app_assets_dir) + 1;
+  return AAssetManager_open(g_asset_manager, asset_path, AASSET_MODE_RANDOM);
+}
+
+AAssetDir* OpenAndroidAssetDir(const char* path) {
+  if (!IsAndroidAssetPath(path) || g_asset_manager == NULL) {
+    return NULL;
+  }
+  const char* asset_path = path + SbStringGetLength(g_app_assets_dir);
+  if (*asset_path == '/') {
+    asset_path++;
+  }
+  return AAssetManager_openDir(g_asset_manager, asset_path);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/file_internal.h b/src/starboard/android/shared/file_internal.h
new file mode 100644
index 0000000..7d33227
--- /dev/null
+++ b/src/starboard/android/shared/file_internal.h
@@ -0,0 +1,57 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
+
+#include <errno.h>
+
+#include <android/asset_manager.h>
+
+#include "starboard/file.h"
+#include "starboard/shared/internal_only.h"
+
+struct SbFilePrivate {
+  // Note: Only one of these two fields will be valid for any given file.
+
+  // The POSIX file descriptor of this file, or -1 if it's a read-only asset.
+  int descriptor;
+
+  // If not NULL this is an Android asset.
+  AAsset* asset;
+
+  SbFilePrivate() : descriptor(-1), asset(NULL) {}
+};
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+extern const char* g_app_assets_dir;
+extern const char* g_app_files_dir;
+extern const char* g_app_cache_dir;
+extern const char* g_app_lib_dir;
+
+void SbFileAndroidInitialize();
+void SbFileAndroidTeardown();
+
+bool IsAndroidAssetPath(const char* path);
+AAsset* OpenAndroidAsset(const char* path);
+AAssetDir* OpenAndroidAssetDir(const char* path);
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_FILE_INTERNAL_H_
diff --git a/src/starboard/android/shared/file_open.cc b/src/starboard/android/shared/file_open.cc
new file mode 100644
index 0000000..83d924a
--- /dev/null
+++ b/src/starboard/android/shared/file_open.cc
@@ -0,0 +1,106 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include <string>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_open.h"
+
+using starboard::android::shared::IsAndroidAssetPath;
+using starboard::android::shared::OpenAndroidAsset;
+
+namespace {
+
+// We don't package most font files in Cobalt content and fallback to the system
+// font file of the same name.
+const std::string kFontsXml("fonts.xml");
+const std::string kSystemFontsDir("/system/fonts/");
+const std::string kCobaltFontsDir("/cobalt/assets/fonts/");
+
+// Returns the fallback for the given asset path, or an empty string if none.
+// NOTE: While Cobalt now provides a mechanism for loading system fonts through
+//       SbSystemGetPath(), using the fallback logic within SbFileOpen() is
+//       still preferred for Android's fonts. The reason for this is that the
+//       Android OS actually allows fonts to be loaded from two locations: one
+//       that it provides; and one that the devices running its OS, which it
+//       calls vendors, can provide. Rather than including the full Android font
+//       package, vendors have the option of using a smaller Android font
+//       package and supplementing it with their own fonts.
+//
+//       If Android were to use SbSystemGetPath() for its fonts, vendors would
+//       have no way of providing those supplemental fonts to Cobalt, which
+//       could result in a limited selection of fonts being available. By
+//       treating Android's fonts as Cobalt's fonts, Cobalt can still offer a
+//       straightforward mechanism for including vendor fonts via
+//       SbSystemGetPath().
+std::string FallbackPath(const std::string& path) {
+  // Fonts fallback to the system fonts.
+  if (path.compare(0, kCobaltFontsDir.length(), kCobaltFontsDir) == 0) {
+    std::string file_name = path.substr(kCobaltFontsDir.length());
+    // fonts.xml doesn't fallback.
+    if (file_name != kFontsXml) {
+      return kSystemFontsDir + file_name;
+    }
+  }
+  return std::string();
+}
+
+}  // namespace
+
+SbFile SbFileOpen(const char* path,
+                  int flags,
+                  bool* out_created,
+                  SbFileError* out_error) {
+  if (!IsAndroidAssetPath(path)) {
+    return ::starboard::shared::posix::impl::FileOpen(
+        path, flags, out_created, out_error);
+  }
+
+  // Assets are never created and are always read-only, whether it's actually an
+  // asset or we end up opening a fallback path.
+  if (out_created) {
+    *out_created = false;
+  }
+  bool can_read = flags & kSbFileRead;
+  bool can_write = flags & kSbFileWrite;
+  if (!can_read || can_write) {
+    if (out_error) {
+      *out_error = kSbFileErrorAccessDenied;
+    }
+    return kSbFileInvalid;
+  }
+
+  AAsset* asset = OpenAndroidAsset(path);
+  if (asset) {
+    SbFile result = new SbFilePrivate();
+    result->asset = asset;
+    return result;
+  }
+
+  std::string fallback_path = FallbackPath(path);
+  if (!fallback_path.empty()) {
+    SbFile result = ::starboard::shared::posix::impl::FileOpen(
+        fallback_path.c_str(), flags, out_created, out_error);
+    return result;
+  }
+
+  if (out_error) {
+    *out_error = kSbFileErrorFailed;
+  }
+  return kSbFileInvalid;
+}
diff --git a/src/starboard/android/shared/file_read.cc b/src/starboard/android/shared/file_read.cc
new file mode 100644
index 0000000..d283019
--- /dev/null
+++ b/src/starboard/android/shared/file_read.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_read.h"
+
+int SbFileRead(SbFile file, char* data, int size) {
+  if (!file || size < 0) {
+    return -1;
+  }
+
+  if (file->asset) {
+    return AAsset_read(file->asset, data, size);
+  } else {
+    return ::starboard::shared::posix::impl::FileRead(file, data, size);
+  }
+}
diff --git a/src/starboard/android/shared/file_seek.cc b/src/starboard/android/shared/file_seek.cc
new file mode 100644
index 0000000..590446b
--- /dev/null
+++ b/src/starboard/android/shared/file_seek.cc
@@ -0,0 +1,28 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include <android/asset_manager.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_seek.h"
+
+int64_t SbFileSeek(SbFile file, SbFileWhence whence, int64_t offset) {
+  if (file && file->asset) {
+    return AAsset_seek64(file->asset, offset, whence);
+  } else {
+    return ::starboard::shared::posix::impl::FileSeek(file, whence, offset);
+  }
+}
diff --git a/src/starboard/android/shared/file_truncate.cc b/src/starboard/android/shared/file_truncate.cc
new file mode 100644
index 0000000..c1075e7
--- /dev/null
+++ b/src/starboard/android/shared/file_truncate.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_truncate.h"
+
+bool SbFileTruncate(SbFile file, int64_t length) {
+  return ::starboard::shared::posix::impl::FileTruncate(file, length);
+}
diff --git a/src/starboard/android/shared/file_write.cc b/src/starboard/android/shared/file_write.cc
new file mode 100644
index 0000000..7705435
--- /dev/null
+++ b/src/starboard/android/shared/file_write.cc
@@ -0,0 +1,22 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/file.h"
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/shared/posix/impl/file_write.h"
+
+int SbFileWrite(SbFile file, const char* data, int size) {
+  return ::starboard::shared::posix::impl::FileWrite(file, data, size);
+}
diff --git a/src/starboard/android/shared/get_home_directory.cc b/src/starboard/android/shared/get_home_directory.cc
new file mode 100644
index 0000000..0d75716
--- /dev/null
+++ b/src/starboard/android/shared/get_home_directory.cc
@@ -0,0 +1,43 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/log.h"
+#include "starboard/shared/nouser/user_internal.h"
+#include "starboard/string.h"
+
+using ::starboard::android::shared::g_app_files_dir;
+
+namespace starboard {
+namespace shared {
+namespace nouser {
+
+// With a single SbUser representing the Android platform, the 'file_storage'
+// SbStorage implementation writes a single SbStorageRecord for all accounts in
+// the home directory we return here.  This is analogous to a web browser
+// providing a single cookie store no matter what any particular web app does to
+// present signing in as different users.
+bool GetHomeDirectory(SbUser user, char* out_path, int path_size) {
+  int len = SbStringCopy(out_path, g_app_files_dir, path_size);
+  return len < path_size;
+}
+
+}  // namespace nouser
+}  // namespace shared
+}  // namespace starboard
diff --git a/src/starboard/android/shared/gyp_configuration.gypi b/src/starboard/android/shared/gyp_configuration.gypi
new file mode 100644
index 0000000..5551a49
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.gypi
@@ -0,0 +1,242 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Platform specific configuration for Android on Starboard.  Automatically
+# included by gyp_cobalt in all .gyp files by Cobalt together with base.gypi.
+#
+{
+  'variables': {
+    'target_os': 'android',
+    'final_executable_type': 'shared_library',
+    'gtest_target_type': 'shared_library',
+
+    'gl_type': 'system_gles2',
+    'enable_remote_debugging': 0,
+
+    # Create v8 snapshot and embed into executable at build-time.
+    'cobalt_v8_buildtime_snapshot': 'true',
+
+    # Define platform specific compiler and linker flags.
+    # Refer to base.gypi for a list of all available variables.
+    'compiler_flags_host': [
+      '-O2',
+    ],
+    'compiler_flags': [
+     # Disable errors for the warning till the Android NDK r19 is fixed.
+     # The warning is trigger when compiling .c files and complains for
+     # '-stdlib=libc++' which is added by the NDK.
+     '-Wno-error=unused-command-line-argument',
+
+      # We'll pretend not to be Linux, but Starboard instead.
+      '-U__linux__',
+
+      # Force char to be signed.
+      '-fsigned-char',
+
+      # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+      '-ffunction-sections',
+      '-funwind-tables',
+      '-fstack-protector-strong',
+      '-no-canonical-prefixes',
+    ],
+    'linker_flags': [
+      '-static-libstdc++',
+
+      # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+      '-Wl,--build-id',
+      '-Wl,--warn-shared-textrel',
+      '-Wl,--fatal-warnings',
+      '-Wl,--gc-sections',
+      '-Wl,-z,nocopyreloc',
+
+      # Facilitate synchronizing punch-out video bounds with the UI frame.
+      '-Wl,--wrap=eglSwapBuffers',
+    ],
+    'compiler_flags_debug': [
+      '-frtti',
+      '-O0',
+    ],
+    'compiler_flags_devel': [
+      '-frtti',
+      '-O2',
+    ],
+    'compiler_flags_qa': [
+      '-fno-rtti',
+      '-O2',
+      '-gline-tables-only',
+    ],
+    'compiler_flags_gold': [
+      '-fno-rtti',
+      '-O2',
+      '-gline-tables-only',
+    ],
+    'platform_libraries': [
+      '-lEGL',
+      '-lGLESv2',
+      '-lOpenSLES',
+      '-landroid',
+      '-llog',
+      '-lmediandk',
+    ],
+    'conditions': [
+      ['clang==1', {
+        'common_clang_flags': [
+          '-Werror',
+          '-fno-exceptions',
+          '-fcolor-diagnostics',
+          '-fno-strict-aliasing',  # See http://crbug.com/32204
+          # Default visibility to hidden, to enable dead stripping.
+          '-fvisibility=hidden',
+          # Warn for implicit type conversions that may change a value.
+          '-Wconversion',
+          # Don't warn about register variables (in base and net)
+          '-Wno-deprecated-register',
+          # Don't warn about deprecated ICU methods (in googleurl and net)
+          '-Wno-deprecated-declarations',
+          # Don't warn about the "struct foo f = {0};" initialization pattern.
+          '-Wno-missing-field-initializers',
+          # Don't warn for implicit sign conversions.
+          '-Wno-sign-conversion',
+          # Triggered by the COMPILE_ASSERT macro.
+          '-Wno-unused-local-typedef',
+          # Don't warn if a function or variable cannot be implicitly
+          # instantiated.
+          '-Wno-undefined-var-template',
+          # Don't warn about DCHECKs comparing against max int in mozjs-45.
+          '-Wno-tautological-constant-out-of-range-compare',
+          # Don't warn about comparing unsigned value < 0 in mozjs-45.
+          '-Wno-tautological-compare',
+          # Don't warn about undefined inlines in mozjs-45.
+          '-Wno-undefined-inline',
+
+          # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+          '-fno-limit-debug-info',
+        ],
+      }],
+      ['cobalt_fastbuild==0', {
+        'compiler_flags_debug': [
+          '-g',
+        ],
+        'compiler_flags_devel': [
+          '-g',
+        ],
+        'compiler_flags_qa': [
+          '-gline-tables-only',
+        ],
+        'compiler_flags_gold': [
+          '-gline-tables-only',
+        ],
+      }],
+    ],
+  },
+
+  'target_defaults': {
+    'cflags_cc': [
+      '-std=c++11',
+    ],
+    'target_conditions': [
+      ['_toolset=="target"', {
+        'defines': [
+          # Cobalt on Linux flag
+          'COBALT_LINUX',
+          '__STDC_FORMAT_MACROS', # so that we get PRI*
+          # Enable GNU extensions to get prototypes like ffsl.
+          '_GNU_SOURCE=1',
+          # Enable compile-time decisions based on the ABI
+          'ANDROID_ABI=<(ANDROID_ABI)',
+          # Note -DANDROID is an argument to some ifdefs in the NDK's eglplatform.h
+          'ANDROID',
+          # Undefining __linux__ causes the system headers to make wrong
+          # assumptions about which C-library is used on the platform.
+          '__BIONIC__',
+          # Undefining __linux__ leaves libc++ without a threads implementation.
+          # TODO: See if there's a way to make libcpp threading use Starboard.
+          '_LIBCPP_HAS_THREAD_API_PTHREAD',
+        ],
+        'cflags': [
+          # libwebp uses the cpufeatures library to detect ARM NEON support
+          '-I<(NDK_HOME)/sources/android/cpufeatures',
+        ],
+      }],
+      ['sb_pedantic_warnings==1', {
+        'cflags': [
+          '-Wall',
+          '-Wextra',
+          '-Wunreachable-code',
+          '<@(common_clang_flags)',
+        ],
+      },{
+        'cflags': [
+          '<@(common_clang_flags)',
+          # 'this' pointer cannot be NULL...pointer may be assumed
+          # to always convert to true.
+          '-Wno-undefined-bool-conversion',
+          # Skia doesn't use overrides.
+          '-Wno-inconsistent-missing-override',
+          # Do not warn about unused function params.
+          '-Wno-unused-parameter',
+          # Do not warn for implicit type conversions that may change a value.
+          '-Wno-conversion',
+          # shifting a negative signed value is undefined
+          '-Wno-shift-negative-value',
+          # Width of bit-field exceeds width of its type- value will be truncated
+          '-Wno-bitfield-width',
+          # Do not warn about an implicit exception spec mismatch.  This is
+          # safe, since we do not enable exceptions.
+          '-Wno-implicit-exception-spec-mismatch',
+        ],
+      }],
+      ['_type=="executable"', {
+        # Android Lollipop+ requires relocatable executables.
+        'cflags': [
+          '-fPIE',
+        ],
+        'ldflags': [
+          '-pie',
+        ],
+      },{
+        # Android requires relocatable shared libraries.
+        'cflags': [
+          '-fPIC',
+        ],
+      }],
+      ['use_asan==1', {
+        'cflags': [
+          '-fsanitize=address',
+          '-fno-omit-frame-pointer',
+        ],
+        'ldflags': [
+          '-fsanitize=address',
+          # Force linking of the helpers in sanitizer_options.cc
+          '-Wl,-u_sanitizer_options_link_helper',
+        ],
+        'defines': [
+          'ADDRESS_SANITIZER',
+        ],
+      }],
+      ['use_tsan==1', {
+        'cflags': [
+          '-fsanitize=thread',
+          '-fno-omit-frame-pointer',
+        ],
+        'ldflags': [
+          '-fsanitize=thread',
+        ],
+        'defines': [
+          'THREAD_SANITIZER',
+        ],
+      }],
+    ],
+  }, # end of target_defaults
+}
diff --git a/src/starboard/android/shared/gyp_configuration.py b/src/starboard/android/shared/gyp_configuration.py
new file mode 100644
index 0000000..ec53474
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.py
@@ -0,0 +1,164 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android shared platform configuration for gyp_cobalt."""
+
+from __future__ import print_function
+
+import imp
+import os
+
+import gyp_utils
+import starboard.android.shared.sdk_utils as sdk_utils
+from starboard.build.platform_configuration import PlatformConfiguration
+from starboard.tools.testing import test_filter
+from subprocess import call
+
+_APK_DIR = os.path.join(os.path.dirname(__file__), os.path.pardir, 'apk')
+_APK_BUILD_ID_FILE = os.path.join(_APK_DIR, 'build.id')
+_COBALT_GRADLE = os.path.join(_APK_DIR, 'cobalt-gradle.sh')
+
+
+class AndroidConfiguration(PlatformConfiguration):
+  """Starboard Android platform configuration."""
+
+  # TODO: make ASAN work with NDK tools and enable it by default
+  def __init__(self, platform, android_abi, asan_enabled_by_default=False):
+    super(AndroidConfiguration, self).__init__(platform,
+                                               asan_enabled_by_default)
+    self.AppendApplicationConfigurationPath(os.path.dirname(__file__))
+
+    self.android_abi = android_abi
+    self.ndk_tools = sdk_utils.GetToolsPath(android_abi)
+
+    self.host_compiler_environment = gyp_utils.GetHostCompilerEnvironment()
+    self.android_home = sdk_utils.GetSdkPath()
+    self.android_ndk_home = sdk_utils.GetNdkPath()
+
+    print('Using Android SDK at {}'.format(self.android_home))
+    print('Using Android NDK at {}'.format(self.android_ndk_home))
+
+  def GetBuildFormat(self):
+    """Returns the desired build format."""
+    # The comma means that ninja and qtcreator_ninja will be chained and use the
+    # same input information so that .gyp files will only have to be parsed
+    # once.
+    return 'ninja,qtcreator_ninja'
+
+  def GetVariables(self, configuration):
+    variables = super(AndroidConfiguration, self).GetVariables(
+        configuration, use_clang=1)
+    variables.update({
+        'ANDROID_HOME':
+            self.android_home,
+        'NDK_HOME':
+            self.android_ndk_home,
+        'ANDROID_ABI':
+            self.android_abi,
+        'include_path_platform_deploy_gypi':
+            'starboard/android/shared/platform_deploy.gypi',
+        'javascript_engine':
+            'v8',
+        'cobalt_enable_jit':
+            1,
+    })
+    return variables
+
+  def GetGeneratorVariables(self, configuration):
+    _ = configuration
+    generator_variables = {
+        'qtcreator_session_name_prefix': 'cobalt',
+    }
+    return generator_variables
+
+  def GetEnvironmentVariables(self):
+    sdk_utils.InstallSdkIfNeeded(self.android_abi)
+    call([_COBALT_GRADLE, '--reset'])
+    with open(_APK_BUILD_ID_FILE, 'w') as build_id_file:
+      build_id_file.write('{}'.format(gyp_utils.GetBuildNumber()))
+
+    env_variables = sdk_utils.GetEnvironmentVariables(self.android_abi)
+    env_variables.update(self.host_compiler_environment)
+    # Android builds tend to consume significantly more memory than the
+    # default settings permit, so cap this at 1 in order to avoid build
+    # issues.  Without this, 32GB machines end up getting automatically
+    # configured to run 5 at a time, which can be too much for at least
+    # android-arm64_debug.
+    # TODO: Eventually replace this with something more robust, like an
+    #       implementation of the abstract toolchain for Android.
+    env_variables.update({'GYP_LINK_CONCURRENCY': '1'})
+
+    return env_variables
+
+  def GetLauncher(self):
+    """Gets the module used to launch applications on this platform."""
+    module_path = os.path.abspath(
+        os.path.join(os.path.dirname(__file__), 'launcher.py'))
+    launcher_module = imp.load_source('launcher', module_path)
+    return launcher_module
+
+  def GetTestFilters(self):
+    filters = super(AndroidConfiguration, self).GetTestFilters()
+    for target, tests in self._FILTERED_TESTS.iteritems():
+      filters.extend(test_filter.TestFilter(target, test) for test in tests)
+    return filters
+
+  # A map of failing or crashing tests per target.
+  _FILTERED_TESTS = {
+      'nplb': [
+          # On Nvidia Shield the AudioFlinger triggers UNDERRUN and
+          # pauses the audio stream.
+          'SbAudioSinkTest.AllFramesConsumed',
+          'SbAudioSinkTest.SomeFramesConsumed',
+          'SbAudioSinkTest.Underflow',
+
+          # Because getifaddrs() isn't in the NDK until API 24, we can't use it
+          # to implement SbSocketGetInterfaceAddress(), and our Java-based
+          # implementation can't return a port for the interface. This test
+          # checks for a 0 port value, so we can't run it on Android with that
+          # incomplete implementation.
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDaySourceForDestination/*',
+      ],
+      'player_filter_tests': [
+          'AudioDecoderTests/AudioDecoderTest.EndOfStreamWithoutAnyInput/0',
+          'AudioDecoderTests/AudioDecoderTest.ResetBeforeInput/0',
+          'AudioDecoderTests/AudioDecoderTest.SingleInput/0',
+          'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/0',
+          'VideoDecoderTests/VideoDecoderTest.DecodeFullGOP/1',
+          'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/0',
+          'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/1',
+          'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/2',
+          'VideoDecoderTests/VideoDecoderTest.EndOfStreamWithoutAnyInput/3',
+          'VideoDecoderTests/VideoDecoderTest'
+          '.GetCurrentDecodeTargetBeforeWriteInputBuffer/0',
+          'VideoDecoderTests/VideoDecoderTest'
+          '.GetCurrentDecodeTargetBeforeWriteInputBuffer/2',
+          'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/0',
+          'VideoDecoderTests/VideoDecoderTest.HoldFramesUntilFull/1',
+
+          # On some platforms, and for some decoders (such as AVC), Android
+          # returns MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER for the test's
+          # invalid input frame instead of signaling an error, which the test is
+          # looking for.
+          'VideoDecoderTests/VideoDecoderTest.SingleInvalidInput/0',
+          'VideoDecoderTests/VideoDecoderTest.SingleInvalidInput/1',
+
+          # Android currently does not support multi-video playback, which
+          # the following tests depend upon.
+          'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/0',
+          'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/1',
+          'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/2',
+          'VideoDecoderTests/VideoDecoderTest.ThreeMoreDecoders/3',
+      ],
+  }
diff --git a/src/starboard/android/shared/input_events_generator.cc b/src/starboard/android/shared/input_events_generator.cc
new file mode 100644
index 0000000..f307963
--- /dev/null
+++ b/src/starboard/android/shared/input_events_generator.cc
@@ -0,0 +1,970 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/input_events_generator.h"
+
+#include <android/input.h>
+#include <android/keycodes.h>
+#include <jni.h>
+#include <math.h>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/double.h"
+#include "starboard/key.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+using ::starboard::shared::starboard::Application;
+typedef ::starboard::android::shared::InputEventsGenerator::Event Event;
+typedef ::starboard::android::shared::InputEventsGenerator::Events Events;
+
+namespace {
+
+SbKeyLocation AInputEventToSbKeyLocation(AInputEvent *event) {
+  int32_t keycode = AKeyEvent_getKeyCode(event);
+  switch (keycode) {
+    case AKEYCODE_ALT_LEFT:
+    case AKEYCODE_CTRL_LEFT:
+    case AKEYCODE_META_LEFT:
+    case AKEYCODE_SHIFT_LEFT:
+      return kSbKeyLocationLeft;
+    case AKEYCODE_ALT_RIGHT:
+    case AKEYCODE_CTRL_RIGHT:
+    case AKEYCODE_META_RIGHT:
+    case AKEYCODE_SHIFT_RIGHT:
+      return kSbKeyLocationRight;
+  }
+  return kSbKeyLocationUnspecified;
+}
+
+unsigned int AInputEventToSbModifiers(AInputEvent *event) {
+  int32_t meta = AKeyEvent_getMetaState(event);
+  unsigned int modifiers = kSbKeyModifiersNone;
+  if (meta & AMETA_ALT_ON) {
+    modifiers |= kSbKeyModifiersAlt;
+  }
+  if (meta & AMETA_CTRL_ON) {
+    modifiers |= kSbKeyModifiersCtrl;
+  }
+  if (meta & AMETA_META_ON) {
+    modifiers |= kSbKeyModifiersMeta;
+  }
+  if (meta & AMETA_SHIFT_ON) {
+    modifiers |= kSbKeyModifiersShift;
+  }
+  return modifiers;
+}
+
+std::unique_ptr<Event> CreateMoveEventWithKey(
+    int32_t device_id,
+    SbWindow window,
+    SbKey key,
+    SbKeyLocation location,
+    const SbInputVector& input_vector) {
+  std::unique_ptr<SbInputData> data(new SbInputData());
+  SbMemorySet(data.get(), 0, sizeof(*data));
+
+  // window
+  data->window = window;
+  data->type = kSbInputEventTypeMove;
+  data->device_type = kSbInputDeviceTypeGamepad;
+  data->device_id = device_id;
+
+  // key
+  data->key = key;
+  data->key_location = location;
+  data->key_modifiers = kSbKeyModifiersNone;
+  data->position = input_vector;
+
+  return std::unique_ptr<Event>(
+      new Application::Event(kSbEventTypeInput, data.release(),
+                             &Application::DeleteDestructor<SbInputData>));
+}
+
+float GetFlat(jobject input_device, int axis) {
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> motion_range(env->CallObjectMethodOrAbort(
+      input_device, "getMotionRange",
+      "(I)Landroid/view/InputDevice$MotionRange;", axis));
+
+  float flat = env->CallFloatMethodOrAbort(
+      motion_range.Get(), "getFlat", "()F");
+
+  SB_DCHECK(flat < 1.0f);
+  return flat;
+}
+
+bool IsDPadKey(SbKey key) {
+  return key == kSbKeyGamepadDPadUp || key == kSbKeyGamepadDPadDown ||
+         key == kSbKeyGamepadDPadLeft || key == kSbKeyGamepadDPadRight;
+}
+
+SbKey AInputEventToSbKey(AInputEvent* event) {
+  int32_t keycode = AKeyEvent_getKeyCode(event);
+  switch (keycode) {
+    // Modifiers
+    case AKEYCODE_ALT_LEFT:
+    case AKEYCODE_ALT_RIGHT:
+    case AKEYCODE_MENU:
+      return kSbKeyMenu;
+    case AKEYCODE_CTRL_LEFT:
+    case AKEYCODE_CTRL_RIGHT:
+      return kSbKeyControl;
+    case AKEYCODE_META_LEFT:
+      return kSbKeyLwin;
+    case AKEYCODE_META_RIGHT:
+      return kSbKeyRwin;
+    case AKEYCODE_SHIFT_LEFT:
+    case AKEYCODE_SHIFT_RIGHT:
+      return kSbKeyShift;
+    case AKEYCODE_CAPS_LOCK:
+      return kSbKeyCapital;
+    case AKEYCODE_NUM_LOCK:
+      return kSbKeyNumlock;
+    case AKEYCODE_SCROLL_LOCK:
+      return kSbKeyScroll;
+
+    // System functions
+    case AKEYCODE_SLEEP:
+      return kSbKeySleep;
+    case AKEYCODE_HELP:
+      return kSbKeyHelp;
+
+    // Navigation
+    case AKEYCODE_BACK:  // Android back button, not backspace
+    case AKEYCODE_ESCAPE:
+      return kSbKeyEscape;
+
+    // Enter/Select
+    case AKEYCODE_ENTER:
+    case AKEYCODE_NUMPAD_ENTER:
+      return kSbKeyReturn;
+
+    // Focus movement
+    case AKEYCODE_PAGE_UP:
+      return kSbKeyPrior;
+    case AKEYCODE_PAGE_DOWN:
+      return kSbKeyNext;
+    case AKEYCODE_MOVE_HOME:
+      return kSbKeyHome;
+    case AKEYCODE_MOVE_END:
+      return kSbKeyEnd;
+
+    // D-pad
+    case AKEYCODE_DPAD_UP:
+      return kSbKeyGamepadDPadUp;
+    case AKEYCODE_DPAD_DOWN:
+      return kSbKeyGamepadDPadDown;
+    case AKEYCODE_DPAD_LEFT:
+      return kSbKeyGamepadDPadLeft;
+    case AKEYCODE_DPAD_RIGHT:
+      return kSbKeyGamepadDPadRight;
+    case AKEYCODE_DPAD_CENTER:
+      return kSbKeyGamepad1;
+
+    // Game controller
+    case AKEYCODE_BUTTON_A:
+      return kSbKeyGamepad1;
+    case AKEYCODE_BUTTON_B:
+      return kSbKeyGamepad2;
+    case AKEYCODE_BUTTON_C:
+      return kSbKeyUnknown;
+    case AKEYCODE_BUTTON_X:
+      return kSbKeyGamepad3;
+    case AKEYCODE_BUTTON_Y:
+      return kSbKeyGamepad4;
+    case AKEYCODE_BUTTON_L1:
+      return kSbKeyGamepadLeftBumper;
+    case AKEYCODE_BUTTON_R1:
+      return kSbKeyGamepadRightBumper;
+    case AKEYCODE_BUTTON_L2:
+      return kSbKeyGamepadLeftTrigger;
+    case AKEYCODE_BUTTON_R2:
+      return kSbKeyGamepadRightTrigger;
+    case AKEYCODE_BUTTON_THUMBL:
+      return kSbKeyGamepadLeftStick;
+    case AKEYCODE_BUTTON_THUMBR:
+      return kSbKeyGamepadRightStick;
+    case AKEYCODE_BUTTON_START:
+      return kSbKeyGamepad6;
+    case AKEYCODE_BUTTON_SELECT:
+      return kSbKeyGamepad5;
+    case AKEYCODE_BUTTON_MODE:
+      return kSbKeyModechange;
+
+    // Media transport
+    case AKEYCODE_MEDIA_PLAY_PAUSE:
+      return kSbKeyMediaPlayPause;
+    case AKEYCODE_MEDIA_PLAY:
+      return kSbKeyPlay;
+    case AKEYCODE_MEDIA_PAUSE:
+      return kSbKeyPause;
+    case AKEYCODE_MEDIA_STOP:
+      return kSbKeyMediaStop;
+    case AKEYCODE_MEDIA_NEXT:
+      return kSbKeyMediaNextTrack;
+    case AKEYCODE_MEDIA_PREVIOUS:
+      return kSbKeyMediaPrevTrack;
+    case AKEYCODE_MEDIA_REWIND:
+      return kSbKeyMediaRewind;
+    case AKEYCODE_MEDIA_FAST_FORWARD:
+      return kSbKeyMediaFastForward;
+
+#if SB_API_VERSION >= 6
+    // TV Remote specific
+    case AKEYCODE_CHANNEL_UP:
+      return kSbKeyChannelUp;
+    case AKEYCODE_CHANNEL_DOWN:
+      return kSbKeyChannelDown;
+    case AKEYCODE_CAPTIONS:
+      return kSbKeyClosedCaption;
+    case AKEYCODE_INFO:
+      return kSbKeyInfo;
+    case AKEYCODE_GUIDE:
+      return kSbKeyGuide;
+    case AKEYCODE_LAST_CHANNEL:
+      return kSbKeyLast;
+    case AKEYCODE_MEDIA_AUDIO_TRACK:
+      return kSbKeyMediaAudioTrack;
+
+    case AKEYCODE_PROG_RED:
+      return kSbKeyRed;
+    case AKEYCODE_PROG_GREEN:
+      return kSbKeyGreen;
+    case AKEYCODE_PROG_YELLOW:
+      return kSbKeyYellow;
+    case AKEYCODE_PROG_BLUE:
+      return kSbKeyBlue;
+#endif  // SB_API_VERSION >= 6
+
+    // Whitespace
+    case AKEYCODE_TAB:
+      return kSbKeyTab;
+    case AKEYCODE_SPACE:
+      return kSbKeySpace;
+
+    // Deletion
+    case AKEYCODE_DEL:  // Backspace
+      return kSbKeyBack;
+    case AKEYCODE_FORWARD_DEL:
+      return kSbKeyDelete;
+    case AKEYCODE_CLEAR:
+      return kSbKeyClear;
+
+    // Insert
+    case AKEYCODE_INSERT:
+      return kSbKeyInsert;
+
+    // Symbols
+    case AKEYCODE_NUMPAD_ADD:
+      return kSbKeyAdd;
+    case AKEYCODE_PLUS:
+    case AKEYCODE_EQUALS:
+    case AKEYCODE_NUMPAD_EQUALS:
+      return kSbKeyOemPlus;
+    case AKEYCODE_NUMPAD_SUBTRACT:
+      return kSbKeySubtract;
+    case AKEYCODE_MINUS:
+      return kSbKeyOemMinus;
+    case AKEYCODE_NUMPAD_MULTIPLY:
+      return kSbKeyMultiply;
+    case AKEYCODE_NUMPAD_DIVIDE:
+      return kSbKeyDivide;
+    case AKEYCODE_COMMA:
+    case AKEYCODE_NUMPAD_COMMA:
+      return kSbKeyOemComma;
+    case AKEYCODE_NUMPAD_DOT:
+      return kSbKeyDecimal;
+    case AKEYCODE_PERIOD:
+      return kSbKeyOemPeriod;
+    case AKEYCODE_SEMICOLON:
+      return kSbKeyOem1;
+    case AKEYCODE_SLASH:
+      return kSbKeyOem2;
+    case AKEYCODE_GRAVE:
+      return kSbKeyOem3;
+    case AKEYCODE_LEFT_BRACKET:
+      return kSbKeyOem4;
+    case AKEYCODE_BACKSLASH:
+      return kSbKeyOem5;
+    case AKEYCODE_RIGHT_BRACKET:
+      return kSbKeyOem6;
+    case AKEYCODE_APOSTROPHE:
+      return kSbKeyOem7;
+
+    // Function keys
+    case AKEYCODE_F1:
+    case AKEYCODE_F2:
+    case AKEYCODE_F3:
+    case AKEYCODE_F4:
+    case AKEYCODE_F5:
+    case AKEYCODE_F6:
+    case AKEYCODE_F7:
+    case AKEYCODE_F8:
+    case AKEYCODE_F9:
+    case AKEYCODE_F10:
+    case AKEYCODE_F11:
+    case AKEYCODE_F12:
+      return static_cast<SbKey>(kSbKeyF1 + (keycode - AKEYCODE_F1));
+
+    // Digits
+    case AKEYCODE_0:
+    case AKEYCODE_1:
+    case AKEYCODE_2:
+    case AKEYCODE_3:
+    case AKEYCODE_4:
+    case AKEYCODE_5:
+    case AKEYCODE_6:
+    case AKEYCODE_7:
+    case AKEYCODE_8:
+    case AKEYCODE_9:
+      return static_cast<SbKey>(kSbKey0 + (keycode - AKEYCODE_0));
+
+    // Numpad digits
+    case AKEYCODE_NUMPAD_0:
+    case AKEYCODE_NUMPAD_1:
+    case AKEYCODE_NUMPAD_2:
+    case AKEYCODE_NUMPAD_3:
+    case AKEYCODE_NUMPAD_4:
+    case AKEYCODE_NUMPAD_5:
+    case AKEYCODE_NUMPAD_6:
+    case AKEYCODE_NUMPAD_7:
+    case AKEYCODE_NUMPAD_8:
+    case AKEYCODE_NUMPAD_9:
+      return static_cast<SbKey>(kSbKeyNumpad0 + (keycode - AKEYCODE_NUMPAD_0));
+
+    // Alphabetic
+    case AKEYCODE_A:
+    case AKEYCODE_B:
+    case AKEYCODE_C:
+    case AKEYCODE_D:
+    case AKEYCODE_E:
+    case AKEYCODE_F:
+    case AKEYCODE_G:
+    case AKEYCODE_H:
+    case AKEYCODE_I:
+    case AKEYCODE_J:
+    case AKEYCODE_K:
+    case AKEYCODE_L:
+    case AKEYCODE_M:
+    case AKEYCODE_N:
+    case AKEYCODE_O:
+    case AKEYCODE_P:
+    case AKEYCODE_Q:
+    case AKEYCODE_R:
+    case AKEYCODE_S:
+    case AKEYCODE_T:
+    case AKEYCODE_U:
+    case AKEYCODE_V:
+    case AKEYCODE_W:
+    case AKEYCODE_X:
+    case AKEYCODE_Y:
+    case AKEYCODE_Z:
+      return static_cast<SbKey>(kSbKeyA + (keycode - AKEYCODE_A));
+
+    // Don't handle these keys so the OS can in a uniform manner.
+    case AKEYCODE_VOLUME_UP:
+    case AKEYCODE_VOLUME_DOWN:
+    case AKEYCODE_MUTE:
+    case AKEYCODE_BRIGHTNESS_UP:
+    case AKEYCODE_BRIGHTNESS_DOWN:
+    case AKEYCODE_SEARCH:
+    default:
+      return kSbKeyUnknown;
+  }
+}
+
+}  // namespace
+
+InputEventsGenerator::InputEventsGenerator(SbWindow window)
+    : window_(window),
+      hat_value_(),
+      left_thumbstick_key_pressed_{kSbKeyUnknown, kSbKeyUnknown} {
+  SB_DCHECK(SbWindowIsValid(window_));
+}
+
+InputEventsGenerator::~InputEventsGenerator() {}
+
+// For a left joystick, AMOTION_EVENT_AXIS_X reports the absolute X position of
+// the joystick. The value is normalized to a range from -1.0 (left) to 1.0
+// (right).
+//
+// For a left joystick, AMOTION_EVENT_AXIS_Y reports the absolute Y position of
+// the joystick. The value is normalized to a range from -1.0 (up or far) to 1.0
+// (down or near).
+//
+// On game pads with two analog joysticks, AMOTION_EVENT_AXIS_Z is often
+// reinterpreted to report the absolute X position of the second joystick.
+//
+// On game pads with two analog joysticks, AMOTION_EVENT_AXIS_RZ is often
+// reinterpreted to report the absolute Y position of the second joystick.
+void InputEventsGenerator::ProcessJoyStickEvent(FlatAxis axis,
+                                                int32_t motion_axis,
+                                                AInputEvent* android_event,
+                                                Events* events) {
+  SB_DCHECK(AMotionEvent_getPointerCount(android_event) > 0);
+
+  int32_t device_id = AInputEvent_getDeviceId(android_event);
+  SB_DCHECK(device_flat_.find(device_id) != device_flat_.end());
+
+  float flat = device_flat_[device_id][axis];
+  float offset = AMotionEvent_getAxisValue(android_event, motion_axis, 0);
+  int sign = offset < 0.0f ? -1 : 1;
+
+  if (SbDoubleAbsolute(offset) < flat) {
+    offset = sign * flat;
+  }
+  // Rescaled the range:
+  // [-1.0f, -flat] to [-1.0f, 0.0f] and [flat, 1.0f] to [0.0f, 1.0f]
+  offset = (offset - sign * flat) / (1 - flat);
+
+  // Report up and left as negative values.
+  SbInputVector input_vector;
+  SbKey key = kSbKeyUnknown;
+  SbKeyLocation location = kSbKeyLocationUnspecified;
+  switch (axis) {
+    case kLeftX: {
+      input_vector.x = offset;
+      input_vector.y = 0.0f;
+      key = kSbKeyGamepadLeftStickLeft;
+      location = kSbKeyLocationLeft;
+      break;
+    }
+    case kLeftY: {
+      input_vector.x = 0.0f;
+      input_vector.y = offset;
+      key = kSbKeyGamepadLeftStickUp;
+      location = kSbKeyLocationLeft;
+      break;
+    }
+    case kRightX: {
+      input_vector.x = offset;
+      input_vector.y = 0.0f;
+      key = kSbKeyGamepadRightStickLeft;
+      location = kSbKeyLocationRight;
+      break;
+    }
+    case kRightY: {
+      input_vector.x = 0.0f;
+      input_vector.y = offset;
+      key = kSbKeyGamepadRightStickUp;
+      location = kSbKeyLocationRight;
+      break;
+    }
+    default:
+      SB_NOTREACHED();
+  }
+
+  events->push_back(
+      CreateMoveEventWithKey(device_id, window_, key, location, input_vector));
+}
+
+namespace {
+
+// Generate a Starboard event from an Android event, with the SbKey and
+// SbInputEventType pre-specified (so that it can be used by event
+// synthesization as well.)
+void PushKeyEvent(SbKey key,
+                  SbInputEventType type,
+                  SbWindow window,
+                  AInputEvent* android_event,
+                  Events* events) {
+  if (key == kSbKeyUnknown) {
+    SB_NOTREACHED();
+    return;
+  }
+
+  std::unique_ptr<SbInputData> data(new SbInputData());
+  SbMemorySet(data.get(), 0, sizeof(*data));
+
+  // window
+  data->window = window;
+  data->type = type;
+
+  // device
+  // TODO: differentiate gamepad, remote, etc.
+  data->device_type = kSbInputDeviceTypeKeyboard;
+  data->device_id = AInputEvent_getDeviceId(android_event);
+
+  // key
+  data->key = key;
+  data->key_location = AInputEventToSbKeyLocation(android_event);
+  data->key_modifiers = AInputEventToSbModifiers(android_event);
+
+  std::unique_ptr<Event> event(
+      new Event(kSbEventTypeInput, data.release(),
+                &Application::DeleteDestructor<SbInputData>));
+  events->push_back(std::move(event));
+}
+
+// Some helper enumerations to index into the InputEventsGenerator::hat_value_
+// array.
+enum HatAxis {
+  kHatX,
+  kHatY,
+};
+
+struct HatValue {
+  HatAxis axis;
+  float value;
+};
+
+// Converts Starboard DPad direction keys to Starboard left thumbstick
+// direction keys.
+SbKey ConvertDPadKeyToThumbstickKey(SbKey key) {
+  switch (key) {
+    case kSbKeyGamepadDPadUp:
+      return kSbKeyGamepadLeftStickUp;
+    case kSbKeyGamepadDPadDown:
+      return kSbKeyGamepadLeftStickDown;
+    case kSbKeyGamepadDPadLeft:
+      return kSbKeyGamepadLeftStickLeft;
+    case kSbKeyGamepadDPadRight:
+      return kSbKeyGamepadLeftStickRight;
+    default: {
+      SB_NOTREACHED();
+      return kSbKeyUnknown;
+    }
+  }
+}
+
+// Convert a Starboard DPad direction key to a (axis, direction) pair.
+HatValue HatValueForDPadKey(SbKey key) {
+  SB_DCHECK(IsDPadKey(key));
+
+  switch (key) {
+    case kSbKeyGamepadDPadUp:
+      return HatValue({kHatY, -1.0f});
+    case kSbKeyGamepadDPadDown:
+      return HatValue({kHatY, 1.0f});
+    case kSbKeyGamepadDPadLeft:
+      return HatValue({kHatX, -1.0f});
+    case kSbKeyGamepadDPadRight:
+      return HatValue({kHatX, 1.0f});
+    default: {
+      SB_NOTREACHED();
+      return HatValue({kHatX, 0.0f});
+    }
+  }
+}
+
+// The inverse of HatValueForDPadKey().
+SbKey KeyForHatValue(const HatValue& hat_value) {
+  SB_DCHECK(hat_value.value > 0.5f || hat_value.value < -0.5f);
+  if (hat_value.axis == kHatX) {
+    if (hat_value.value > 0.5f) {
+      return kSbKeyGamepadDPadRight;
+    } else {
+      return kSbKeyGamepadDPadLeft;
+    }
+  } else if (hat_value.axis == kHatY) {
+    if (hat_value.value > 0.5f) {
+      return kSbKeyGamepadDPadDown;
+    } else {
+      return kSbKeyGamepadDPadUp;
+    }
+  } else {
+    SB_NOTREACHED();
+    return kSbKeyUnknown;
+  }
+}
+
+// Analyzes old axis values and new axis values and fire off any synthesized
+// key press/unpress events as necessary.
+void PossiblySynthesizeHatKeyEvents(HatAxis axis,
+                                    float old_value,
+                                    float new_value,
+                                    SbWindow window,
+                                    AInputEvent* android_event,
+                                    Events* events) {
+  if (old_value == new_value) {
+    // No events to generate if the hat motion value did not change.
+    return;
+  }
+
+  if (old_value > 0.5f || old_value < -0.5f) {
+    PushKeyEvent(KeyForHatValue(HatValue({axis, old_value})),
+                 kSbInputEventTypeUnpress, window, android_event, events);
+  }
+  if (new_value > 0.5f || new_value < -0.5f) {
+    PushKeyEvent(KeyForHatValue(HatValue({axis, new_value})),
+                 kSbInputEventTypePress, window, android_event, events);
+  }
+}
+
+}  // namespace
+
+bool InputEventsGenerator::ProcessKeyEvent(AInputEvent* android_event,
+                                           Events* events) {
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+  if (!input_events_filter_.ShouldProcessKeyEvent(android_event)) {
+    return false;
+  }
+#endif
+
+  SbInputEventType type;
+  switch (AKeyEvent_getAction(android_event)) {
+    case AKEY_EVENT_ACTION_DOWN:
+      type = kSbInputEventTypePress;
+      break;
+    case AKEY_EVENT_ACTION_UP:
+      type = kSbInputEventTypeUnpress;
+      break;
+    default:
+      // TODO: send multiple events for AKEY_EVENT_ACTION_MULTIPLE
+      return false;
+  }
+
+  SbKey key = AInputEventToSbKey(android_event);
+  if (key == kSbKeyUnknown) {
+    return false;
+  }
+
+  if (AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK &&
+      IsDPadKey(key)) {
+    // For fallback DPad keys, we flow into special processing to manage the
+    // differentiation between the actual DPad and the left thumbstick, since
+    // Android conflates the key down/up events for these inputs.
+    ProcessFallbackDPadEvent(type, key, android_event, events);
+  } else {
+    PushKeyEvent(key, type, window_, android_event, events);
+  }
+  return true;
+}
+
+namespace {
+
+SbKey ButtonStateToSbKey(int32_t button_state) {
+  if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
+    return kSbKeyMouse1;
+  } else if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
+    return kSbKeyMouse2;
+  } else if (button_state & AMOTION_EVENT_BUTTON_TERTIARY) {
+    return kSbKeyMouse3;
+  } else if (button_state & AMOTION_EVENT_BUTTON_BACK) {
+    return kSbKeyBrowserBack;
+  } else if (button_state & AMOTION_EVENT_BUTTON_FORWARD) {
+    return kSbKeyBrowserForward;
+  }
+  return kSbKeyUnknown;
+}
+
+// Get an SbKeyModifiers from a button state
+unsigned int ButtonStateToSbModifiers(unsigned int button_state) {
+  unsigned int key_modifiers = kSbKeyModifiersNone;
+#if SB_API_VERSION >= 6
+  if (button_state & AMOTION_EVENT_BUTTON_PRIMARY) {
+    key_modifiers |= kSbKeyModifiersPointerButtonLeft;
+  }
+  if (button_state & AMOTION_EVENT_BUTTON_SECONDARY) {
+    key_modifiers |= kSbKeyModifiersPointerButtonMiddle;
+  }
+  if (button_state & AMOTION_EVENT_BUTTON_TERTIARY) {
+    key_modifiers |= kSbKeyModifiersPointerButtonRight;
+  }
+  if (button_state & AMOTION_EVENT_BUTTON_BACK) {
+    key_modifiers |= kSbKeyModifiersPointerButtonBack;
+  }
+  if (button_state & AMOTION_EVENT_BUTTON_FORWARD) {
+    key_modifiers |= kSbKeyModifiersPointerButtonForward;
+  }
+#endif
+  return key_modifiers;
+}
+
+#if SB_API_VERSION < 6
+SbKey ScrollAxisToKey(float hscroll, float vscroll) {
+  if (vscroll != 0) {
+    return vscroll < 0 ? kSbKeyDown : kSbKeyUp;
+  } else if (hscroll != 0) {
+    return hscroll > 0 ? kSbKeyLeft : kSbKeyRight;
+  }
+  return kSbKeyUnknown;
+}
+#endif
+
+}  // namespace
+
+bool InputEventsGenerator::ProcessPointerEvent(AInputEvent* android_event,
+                                               Events* events) {
+  float offset_x =
+      AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_X, 0);
+  float offset_y =
+      AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_Y, 0);
+
+  std::unique_ptr<SbInputData> data(new SbInputData());
+  SbMemorySet(data.get(), 0, sizeof(*data));
+
+  data->window = window_;
+  SB_DCHECK(SbWindowIsValid(data->window));
+#if SB_API_VERSION >= 6
+  data->pressure = NAN;
+  data->size = {NAN, NAN};
+  data->tilt = {NAN, NAN};
+#endif
+  unsigned int button_state = AMotionEvent_getButtonState(android_event);
+  unsigned int button_modifiers = ButtonStateToSbModifiers(button_state);
+
+  // Default to reporting pointer events as mouse events.
+  data->device_type = kSbInputDeviceTypeMouse;
+
+  // Report both stylus and touchscreen events as touchscreen device events.
+  int32_t event_source = AInputEvent_getSource(android_event);
+  if (((event_source & AINPUT_SOURCE_TOUCHSCREEN) != 0) ||
+      ((event_source & AINPUT_SOURCE_STYLUS) != 0)) {
+    data->device_type = kSbInputDeviceTypeTouchScreen;
+  }
+
+  data->device_id = AInputEvent_getDeviceId(android_event);
+  data->key_modifiers =
+      button_modifiers | AInputEventToSbModifiers(android_event);
+  data->position.x = offset_x;
+  data->position.y = offset_y;
+  data->key = ButtonStateToSbKey(button_state);
+
+  switch (AKeyEvent_getAction(android_event) & AMOTION_EVENT_ACTION_MASK) {
+    case AMOTION_EVENT_ACTION_UP:
+      data->type = kSbInputEventTypeUnpress;
+      break;
+    case AMOTION_EVENT_ACTION_DOWN:
+      data->type = kSbInputEventTypePress;
+      break;
+    case AMOTION_EVENT_ACTION_MOVE:
+    case AMOTION_EVENT_ACTION_HOVER_MOVE:
+      data->type = kSbInputEventTypeMove;
+      break;
+    case AMOTION_EVENT_ACTION_SCROLL: {
+      float hscroll = AMotionEvent_getAxisValue(
+          android_event, AMOTION_EVENT_AXIS_HSCROLL, 0);  // left is -1
+      float vscroll = AMotionEvent_getAxisValue(
+          android_event, AMOTION_EVENT_AXIS_VSCROLL, 0);  // down is -1
+      float wheel =
+          AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_WHEEL, 0);
+#if SB_API_VERSION >= 6
+      data->type = kSbInputEventTypeWheel;
+      data->key = kSbKeyUnknown;
+      data->delta.y = -vscroll;
+      data->delta.x = hscroll;
+#else
+      // This version of Starboard does not support wheel event types, send
+      // keyboard event types instead.
+      data->device_type = kSbInputDeviceTypeKeyboard;
+      data->key = ScrollAxisToKey(hscroll, vscroll);
+
+      std::unique_ptr<SbInputData> data_press(new SbInputData());
+      SbMemoryCopy(data_press.get(), data.get(), sizeof(*data_press));
+
+      // Send a press and unpress event.
+      data_press->type = kSbInputEventTypePress;
+      events->push_back(std::unique_ptr<Event>(
+          new Application::Event(kSbEventTypeInput, data_press.release(),
+                                 &Application::DeleteDestructor<SbInputData>)));
+
+      data->type = kSbInputEventTypeUnpress;
+#endif
+      break;
+    }
+    default:
+      return false;
+  }
+
+  events->push_back(std::unique_ptr<Event>(
+      new Application::Event(kSbEventTypeInput, data.release(),
+                             &Application::DeleteDestructor<SbInputData>)));
+  return true;
+}
+
+bool InputEventsGenerator::ProcessMotionEvent(AInputEvent* android_event,
+                                              Events* events) {
+  int32_t event_source = AInputEvent_getSource(android_event);
+  if ((event_source & AINPUT_SOURCE_CLASS_POINTER) != 0) {
+    return ProcessPointerEvent(android_event, events);
+  }
+  if ((event_source & AINPUT_SOURCE_JOYSTICK) == 0) {
+    // Only handles joystick events in the code below.
+    return false;
+  }
+
+  UpdateDeviceFlatMapIfNecessary(android_event);
+  ProcessJoyStickEvent(kLeftX, AMOTION_EVENT_AXIS_X, android_event, events);
+  ProcessJoyStickEvent(kLeftY, AMOTION_EVENT_AXIS_Y, android_event, events);
+  ProcessJoyStickEvent(kRightX, AMOTION_EVENT_AXIS_Z, android_event, events);
+  ProcessJoyStickEvent(kRightY, AMOTION_EVENT_AXIS_RZ, android_event, events);
+
+  // Remember the "hat" input values (dpad on the game controller) to help
+  // differentiate hat vs. stick fallback events.
+  UpdateHatValuesAndPossiblySynthesizeKeyEvents(android_event, events);
+
+  // Lie to Android and tell it that we did not process the motion event,
+  // causing Android to synthesize dpad key events for us. When we handle
+  // those synthesized key events we'll enqueue kSbKeyGamepadLeft rather
+  // than kSbKeyGamepadDPad events if they're from the joystick.
+  return false;
+}
+
+// Special processing to disambiguate between DPad events and left-thumbstick
+// direction key events.
+void InputEventsGenerator::ProcessFallbackDPadEvent(SbInputEventType type,
+                                                    SbKey key,
+                                                    AInputEvent* android_event,
+                                                    Events* events) {
+  SB_DCHECK(AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK);
+  SB_DCHECK(IsDPadKey(key));
+
+  HatAxis hat_axis = HatValueForDPadKey(key).axis;
+
+  if (hat_value_[hat_axis] != 0.0f && type == kSbInputEventTypePress) {
+    // Direction pad events are all assumed to be coming from the hat controls
+    // if motion events for that hat DPAD is active, but we do still handle
+    // repeat keys here.
+    if (AKeyEvent_getRepeatCount(android_event) > 0) {
+      SB_LOG(INFO) << AKeyEvent_getRepeatCount(android_event);
+      PushKeyEvent(key, kSbInputEventTypePress, window_, android_event, events);
+    }
+    return;
+  }
+
+  // If we get this far, then we are exclusively dealing with thumbstick events,
+  // as actual DPad events are processed in motion events by checking the
+  // hat axis representing the DPad.
+  SbKey thumbstick_key = ConvertDPadKeyToThumbstickKey(key);
+
+  if (left_thumbstick_key_pressed_[hat_axis] != kSbKeyUnknown &&
+      (type == kSbInputEventTypeUnpress ||
+       left_thumbstick_key_pressed_[hat_axis] != thumbstick_key)) {
+    // Fire an unpressed event if our current key differs from the last seen
+    // key.
+    PushKeyEvent(left_thumbstick_key_pressed_[hat_axis],
+                 kSbInputEventTypeUnpress, window_, android_event, events);
+  }
+
+  if (type == kSbInputEventTypePress) {
+    PushKeyEvent(thumbstick_key, kSbInputEventTypePress, window_,
+                 android_event, events);
+    left_thumbstick_key_pressed_[hat_axis] = thumbstick_key;
+  } else if (type == kSbInputEventTypeUnpress) {
+    left_thumbstick_key_pressed_[hat_axis] = kSbKeyUnknown;
+  } else {
+    SB_NOTREACHED();
+  }
+}
+
+// Update |InputEventsGenerator::hat_value_| according to the incoming motion
+// event's data.  Possibly generate DPad events based on any changes in value
+// here.
+void InputEventsGenerator::UpdateHatValuesAndPossiblySynthesizeKeyEvents(
+    AInputEvent* android_event,
+    Events* events) {
+  float new_hat_x =
+      AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_X, 0);
+  PossiblySynthesizeHatKeyEvents(kHatX, hat_value_[kHatX], new_hat_x, window_,
+                                 android_event, events);
+  hat_value_[kHatX] = new_hat_x;
+
+  float new_hat_y =
+      AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_Y, 0);
+  PossiblySynthesizeHatKeyEvents(kHatY, hat_value_[kHatY], new_hat_y, window_,
+                                 android_event, events);
+  hat_value_[kHatY] = new_hat_y;
+}
+
+void InputEventsGenerator::UpdateDeviceFlatMapIfNecessary(
+    AInputEvent* android_event) {
+  int32_t device_id = AInputEvent_getDeviceId(android_event);
+  if (device_flat_.find(device_id) != device_flat_.end()) {
+    // |device_flat_| is already contains the device flat information.
+    return;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> input_device(env->CallStaticObjectMethodOrAbort(
+      "android/view/InputDevice", "getDevice", "(I)Landroid/view/InputDevice;",
+      device_id));
+  float flats[kNumAxes] = {GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_X),
+                           GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_Y),
+                           GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_Z),
+                           GetFlat(input_device.Get(), AMOTION_EVENT_AXIS_RZ)};
+  device_flat_[device_id] = std::vector<float>(flats, flats + kNumAxes);
+}
+
+bool InputEventsGenerator::CreateInputEventsFromAndroidEvent(
+    AInputEvent* android_event,
+    Events* events) {
+  if (android_event == NULL ||
+      (AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_KEY &&
+       AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_MOTION)) {
+    return false;
+  }
+
+  switch (AInputEvent_getType(android_event)) {
+    case AINPUT_EVENT_TYPE_KEY:
+      return ProcessKeyEvent(android_event, events);
+    case AINPUT_EVENT_TYPE_MOTION: {
+      return ProcessMotionEvent(android_event, events);
+    }
+    default:
+      SB_NOTREACHED();
+  }
+
+  return false;
+}
+
+void InputEventsGenerator::CreateInputEventsFromSbKey(SbKey key,
+                                                      Events* events) {
+  events->clear();
+
+  // Press event
+  std::unique_ptr<SbInputData> data(new SbInputData());
+  SbMemorySet(data.get(), 0, sizeof(*data));
+
+  data->window = window_;
+  data->type = kSbInputEventTypePress;
+
+  data->device_type = kSbInputDeviceTypeKeyboard;
+  data->device_id = 0;
+
+  data->key = key;
+  data->key_location = kSbKeyLocationUnspecified;
+  data->key_modifiers = kSbKeyModifiersNone;
+
+  events->push_back(std::unique_ptr<Event>(
+      new Application::Event(kSbEventTypeInput, data.release(),
+                             &Application::DeleteDestructor<SbInputData>)));
+
+  // Unpress event
+  data.reset(new SbInputData());
+  SbMemorySet(data.get(), 0, sizeof(*data));
+
+  data->window = window_;
+  data->type = kSbInputEventTypeUnpress;
+
+  data->device_type = kSbInputDeviceTypeKeyboard;
+  data->device_id = 0;
+
+  data->key = key;
+  data->key_location = kSbKeyLocationUnspecified;
+  data->key_modifiers = kSbKeyModifiersNone;
+
+  events->push_back(std::unique_ptr<Event>(
+      new Application::Event(kSbEventTypeInput, data.release(),
+                             &Application::DeleteDestructor<SbInputData>)));
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/input_events_generator.h b/src/starboard/android/shared/input_events_generator.h
new file mode 100644
index 0000000..a3f6aef
--- /dev/null
+++ b/src/starboard/android/shared/input_events_generator.h
@@ -0,0 +1,101 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
+#define STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
+
+#include <android/input.h>
+#include <map>
+#include <memory>
+#include <vector>
+
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+#include "starboard/android/shared/input_events_filter.h"
+#endif
+
+#include "starboard/input.h"
+#include "starboard/shared/starboard/application.h"
+#include "starboard/window.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class InputEventsGenerator {
+ public:
+  typedef ::starboard::shared::starboard::Application::Event Event;
+  typedef std::vector<std::unique_ptr<Event> > Events;
+
+  explicit InputEventsGenerator(SbWindow window);
+  virtual ~InputEventsGenerator();
+
+  // Translates an Android input event into a series of Starboard application
+  // events. The caller owns the new events and must delete them when done with
+  // them.
+  bool CreateInputEventsFromAndroidEvent(AInputEvent* android_event,
+                                         Events* events);
+
+  // Create press/unpress events from SbKey
+  // (for use with CobaltA11yHelper injection)
+  void CreateInputEventsFromSbKey(SbKey key, Events* events);
+
+ private:
+  enum FlatAxis {
+    kLeftX,
+    kLeftY,
+    kRightX,
+    kRightY,
+    kNumAxes,
+  };
+
+  bool ProcessKeyEvent(AInputEvent* android_event, Events* events);
+  bool ProcessPointerEvent(AInputEvent* android_event, Events* events);
+  bool ProcessMotionEvent(AInputEvent* android_event, Events* events);
+  void ProcessJoyStickEvent(FlatAxis axis,
+                            int32_t motion_axis,
+                            AInputEvent* android_event,
+                            Events* events);
+  void UpdateDeviceFlatMapIfNecessary(AInputEvent* android_event);
+
+  void ProcessFallbackDPadEvent(SbInputEventType type,
+                                SbKey key,
+                                AInputEvent* android_event,
+                                Events* events);
+  void UpdateHatValuesAndPossiblySynthesizeKeyEvents(AInputEvent* android_event,
+                                                     Events* events);
+
+  SbWindow window_;
+
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+  InputEventsFilter input_events_filter_;
+#endif
+
+  // Map the device id with joystick flat position.
+  // Cache the flat area of joystick to avoid calling jni functions frequently.
+  std::map<int32_t, std::vector<float> > device_flat_;
+
+  // The curent X/Y analog values of the "hat" (dpad on the game controller).
+  float hat_value_[2];
+
+  // The last known value of the left thumbstick, used to track when we should
+  // generate key unpressed events for it.  We store values for horizontal and
+  // vertical directions independently.
+  SbKey left_thumbstick_key_pressed_[2];
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
diff --git a/src/starboard/android/shared/jni_env_ext.cc b/src/starboard/android/shared/jni_env_ext.cc
new file mode 100644
index 0000000..a42359e
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext.cc
@@ -0,0 +1,107 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <algorithm>
+#include <string>
+
+#include "starboard/thread.h"
+
+namespace {
+
+SbThreadLocalKey g_tls_key = kSbThreadLocalKeyInvalid;
+JavaVM* g_vm = NULL;
+jobject g_application_class_loader = NULL;
+jobject g_starboard_bridge = NULL;
+
+void Destroy(void* value) {
+  // OnThreadShutdown() must be called on each thread before it is destroyed.
+  SB_DCHECK(value == NULL);
+}
+
+}  // namespace
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// static
+void JniEnvExt::Initialize(JniEnvExt* env, jobject starboard_bridge) {
+  SB_DCHECK(g_tls_key == kSbThreadLocalKeyInvalid);
+  g_tls_key = SbThreadCreateLocalKey(Destroy);
+
+  SB_DCHECK(g_vm == NULL);
+  env->GetJavaVM(&g_vm);
+
+  SB_DCHECK(g_application_class_loader == NULL);
+  g_application_class_loader = env->ConvertLocalRefToGlobalRef(
+      env->CallObjectMethodOrAbort(env->GetObjectClass(starboard_bridge),
+                                   "getClassLoader",
+                                   "()Ljava/lang/ClassLoader;"));
+
+  SB_DCHECK(g_starboard_bridge == NULL);
+  g_starboard_bridge = env->NewGlobalRef(starboard_bridge);
+}
+
+// static
+void JniEnvExt::OnThreadShutdown() {
+  // We must call DetachCurrentThread() before exiting, if we have ever
+  // previously called AttachCurrentThread() on it.
+  //   http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html
+  if (SbThreadGetLocalValue(g_tls_key)) {
+    g_vm->DetachCurrentThread();
+    SbThreadSetLocalValue(g_tls_key, NULL);
+  }
+}
+
+JniEnvExt* JniEnvExt::Get() {
+  JNIEnv* env;
+  if (JNI_OK != g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) {
+    // Tell the JVM our thread name so it doesn't change it.
+    char thread_name[16];
+    SbThreadGetName(thread_name, sizeof(thread_name));
+    JavaVMAttachArgs args { JNI_VERSION_1_6, thread_name, NULL };
+    g_vm->AttachCurrentThread(&env, &args);
+    // We don't use the value, but any non-NULL means we have to detach.
+    SbThreadSetLocalValue(g_tls_key, env);
+  }
+  // The downcast is safe since we only add methods, not fields.
+  return static_cast<JniEnvExt*>(env);
+}
+
+jobject JniEnvExt::GetStarboardBridge() {
+  return g_starboard_bridge;
+}
+
+jclass JniEnvExt::FindClassExtOrAbort(const char* name) {
+  // Convert the JNI FindClass name with slashes to the "binary name" with dots
+  // for ClassLoader.loadClass().
+  ::std::string dot_name = name;
+  ::std::replace(dot_name.begin(), dot_name.end(), '/', '.');
+  jstring jname = NewStringUTF(dot_name.c_str());
+  AbortOnException();
+  jobject clazz_obj =
+      CallObjectMethodOrAbort(g_application_class_loader, "loadClass",
+                              "(Ljava/lang/String;)Ljava/lang/Class;", jname);
+  DeleteLocalRef(jname);
+  return static_cast<jclass>(clazz_obj);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/jni_env_ext.h b/src/starboard/android/shared/jni_env_ext.h
new file mode 100644
index 0000000..3797c08
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext.h
@@ -0,0 +1,387 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
+#define STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <cstdarg>
+#include <cstring>
+#include <string>
+
+#include "starboard/log.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// An extension to JNIEnv to simplify making JNI calls.
+//
+// Call the static Get() method to get an instance that is already attached to
+// the JVM in the current thread.
+//
+// This extends the JNIEnv structure, which already has a C++ interface for
+// calling JNI methods, so any JNIEnv method can be called directly on this.
+//
+// There are convenience methods to lookup and call Java methods on object
+// instances in a single step, with even simpler methods to call Java methods on
+// the StarboardBridge.
+struct JniEnvExt : public JNIEnv {
+  // One-time initialization to be called before starting the application.
+  static void Initialize(JniEnvExt* jni_env, jobject starboard_bridge);
+
+  // Called right before each native thread is about to be shutdown.
+  static void OnThreadShutdown();
+
+  // Returns the thread-specific instance of JniEnvExt.
+  static JniEnvExt* Get();
+
+  // Returns the StarboardBridge object.
+  jobject GetStarboardBridge();
+
+  // Lookup the class of an object and find a field in it.
+  jfieldID GetStaticFieldIDOrAbort(jclass clazz,
+                                   const char* name,
+                                   const char* sig) {
+    jfieldID field = GetStaticFieldID(clazz, name, sig);
+    AbortOnException();
+    return field;
+  }
+
+  jfieldID GetFieldIDOrAbort(jobject obj,
+                             const char* name,
+                             const char* sig) {
+    jclass clazz = GetObjectClass(obj);
+    AbortOnException();
+    jfieldID field = GetFieldID(clazz, name, sig);
+    AbortOnException();
+    DeleteLocalRef(clazz);
+    return field;
+  }
+
+  jint GetEnumValueOrAbort(jclass clazz, const char* name) {
+    jfieldID field = GetStaticFieldIDOrAbort(clazz, name, "I");
+    jint enum_value = GetStaticIntField(clazz, field);
+    AbortOnException();
+    return enum_value;
+  }
+
+  // Lookup the class of an object and find a method in it.
+  jmethodID GetObjectMethodIDOrAbort(jobject obj,
+                                     const char* name,
+                                     const char* sig) {
+    jclass clazz = GetObjectClass(obj);
+    AbortOnException();
+    jmethodID method_id = GetMethodID(clazz, name, sig);
+    AbortOnException();
+    DeleteLocalRef(clazz);
+    return method_id;
+  }
+
+  jmethodID GetStaticMethodIDOrAbort(jclass clazz,
+                                     const char* name,
+                                     const char* sig) {
+    jmethodID method = GetStaticMethodID(clazz, name, sig);
+    AbortOnException();
+    return method;
+  }
+
+  jobject GetObjectArrayElementOrAbort(jobjectArray array, jsize index) {
+    jobject result = GetObjectArrayElement(array, index);
+    AbortOnException();
+    return result;
+  }
+
+  // Find a class by name using the application's class loader. This can load
+  // both system classes and application classes, even when not in a JNI
+  // stack frame (e.g. in a native thread that was attached the the JVM).
+  // https://developer.android.com/training/articles/perf-jni.html#faq_FindClass
+  jclass FindClassExtOrAbort(const char* name);
+
+  jclass FindClassOrAbort(const char* name) {
+    jclass result = FindClass(name);
+    AbortOnException();
+    return result;
+  }
+
+  // Convenience method to lookup and call a constructor.
+  jobject NewObjectOrAbort(const char* class_name, const char* sig, ...) {
+    va_list argp;
+    va_start(argp, sig);
+    jclass clazz = FindClassExtOrAbort(class_name);
+    jmethodID methodID = GetMethodID(clazz, "<init>", sig);
+    AbortOnException();
+    jobject result = NewObjectV(clazz, methodID, argp);
+    AbortOnException();
+    DeleteLocalRef(clazz);
+    va_end(argp);
+    return result;
+  }
+
+  // Constructs a new java.lang.String object from an array of characters in
+  // standard UTF-8 encoding. This differs from JNIEnv::NewStringUTF() which
+  // takes JNI modified UTF-8.
+  jstring NewStringStandardUTFOrAbort(const char* bytes) {
+    const jstring charset = NewStringUTF("UTF-8");
+    AbortOnException();
+    const jbyteArray byte_array = NewByteArrayFromRaw(
+        reinterpret_cast<const jbyte*>(bytes), strlen(bytes));
+    AbortOnException();
+    jstring result = static_cast<jstring>(NewObjectOrAbort(
+        "java/lang/String", "([BLjava/lang/String;)V", byte_array, charset));
+    DeleteLocalRef(byte_array);
+    DeleteLocalRef(charset);
+    return result;
+  }
+
+  // Returns a std::string representing the jstring in standard UTF-8 encoding.
+  // This differs from JNIEnv::GetStringUTFChars() which returns modified UTF-8.
+  // Also, the buffer of the returned bytes is managed by the std::string object
+  // so it is not necessary to release it with JNIEnv::ReleaseStringUTFChars().
+  std::string GetStringStandardUTFOrAbort(jstring str) {
+    if (str == NULL) {
+      return std::string();
+    }
+    const jstring charset = NewStringUTF("UTF-8");
+    AbortOnException();
+    const jbyteArray byte_array = static_cast<jbyteArray>(
+        CallObjectMethodOrAbort(str, "getBytes", "(Ljava/lang/String;)[B",
+                                charset));
+    jsize array_length = GetArrayLength(byte_array);
+    AbortOnException();
+    void* bytes = GetPrimitiveArrayCritical(byte_array, NULL);
+    AbortOnException();
+    std::string result(static_cast<const char*>(bytes), array_length);
+    ReleasePrimitiveArrayCritical(byte_array, bytes, JNI_ABORT);
+    AbortOnException();
+    DeleteLocalRef(byte_array);
+    DeleteLocalRef(charset);
+    return result;
+  }
+
+// Convenience methods to lookup and read a field or call a method all at once:
+// Get[Type]FieldOrAbort() takes a jobject of an instance.
+// Call[Type]MethodOrAbort() takes a jobject of an instance.
+// CallStarboard[Type]MethodOrAbort() to call methods on the StarboardBridge.
+#define X(_jtype, _jname)                                                      \
+  _jtype Get##_jname##FieldOrAbort(jobject obj, const char* name,              \
+                                   const char* sig) {                          \
+    _jtype result = Get##_jname##Field(obj, GetFieldIDOrAbort(obj, name, sig));\
+    AbortOnException();                                                        \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype GetStatic##_jname##FieldOrAbort(const char* class_name,               \
+                                         const char* name, const char* sig) {  \
+    jclass clazz = FindClassExtOrAbort(class_name);                            \
+    return GetStatic##_jname##FieldOrAbort(clazz, name, sig);                  \
+  }                                                                            \
+                                                                               \
+  _jtype GetStatic##_jname##FieldOrAbort(jclass clazz, const char* name,       \
+                                         const char* sig) {                    \
+    _jtype result = GetStatic##_jname##Field(                                  \
+        clazz, GetStaticFieldIDOrAbort(clazz, name, sig));                     \
+    AbortOnException();                                                        \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype Call##_jname##MethodOrAbort(jobject obj, const char* name,            \
+                                     const char* sig, ...) {                   \
+    va_list argp;                                                              \
+    va_start(argp, sig);                                                       \
+    _jtype result = Call##_jname##MethodVOrAbort(                              \
+        obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);                  \
+    va_end(argp);                                                              \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype CallStarboard##_jname##MethodOrAbort(const char* name,                \
+                                             const char* sig, ...) {           \
+    va_list argp;                                                              \
+    va_start(argp, sig);                                                       \
+    jobject obj = GetStarboardBridge();                                        \
+    _jtype result = Call##_jname##MethodVOrAbort(                              \
+        obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);                  \
+    va_end(argp);                                                              \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype CallStatic##_jname##MethodOrAbort(                                    \
+      const char* class_name, const char* method_name, const char* sig, ...) { \
+    va_list argp;                                                              \
+    va_start(argp, sig);                                                       \
+    jclass clazz = FindClassExtOrAbort(class_name);                            \
+    _jtype result = CallStatic##_jname##MethodVOrAbort(                        \
+        clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp);       \
+    DeleteLocalRef(clazz);                                                     \
+    va_end(argp);                                                              \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype Call##_jname##MethodVOrAbort(jobject obj, jmethodID methodID,         \
+                                      va_list args) {                          \
+    _jtype result = Call##_jname##MethodV(obj, methodID, args);                \
+    AbortOnException();                                                        \
+    return result;                                                             \
+  }                                                                            \
+                                                                               \
+  _jtype CallStatic##_jname##MethodVOrAbort(jclass clazz, jmethodID methodID,  \
+                                            va_list args) {                    \
+    _jtype result = CallStatic##_jname##MethodV(clazz, methodID, args);        \
+    AbortOnException();                                                        \
+    return result;                                                             \
+  }
+
+  X(jobject, Object)
+  X(jboolean, Boolean)
+  X(jbyte, Byte)
+  X(jchar, Char)
+  X(jshort, Short)
+  X(jint, Int)
+  X(jlong, Long)
+  X(jfloat, Float)
+  X(jdouble, Double)
+
+#undef X
+
+  void CallVoidMethod(jobject obj, const char* name, const char* sig, ...) {
+    va_list argp;
+    va_start(argp, sig);
+    CallVoidMethodV(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+    va_end(argp);
+  }
+
+  void CallVoidMethodOrAbort(jobject obj,
+                             const char* name,
+                             const char* sig,
+                             ...) {
+    va_list argp;
+    va_start(argp, sig);
+    CallVoidMethodVOrAbort(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+    va_end(argp);
+  }
+
+  void CallVoidMethodVOrAbort(jobject obj, jmethodID methodID, va_list args) {
+    CallVoidMethodV(obj, methodID, args);
+    AbortOnException();
+  }
+
+  void CallStarboardVoidMethod(const char* name, const char* sig, ...) {
+    va_list argp;
+    va_start(argp, sig);
+    jobject obj = GetStarboardBridge();
+    CallVoidMethodV(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+    va_end(argp);
+  }
+
+  void CallStarboardVoidMethodOrAbort(const char* name, const char* sig, ...) {
+    va_list argp;
+    va_start(argp, sig);
+    jobject obj = GetStarboardBridge();
+    CallVoidMethodVOrAbort(obj, GetObjectMethodIDOrAbort(obj, name, sig), argp);
+    va_end(argp);
+  }
+
+  void CallStaticVoidMethod(const char* class_name,
+                            const char* method_name,
+                            const char* sig,
+                            ...) {
+    va_list argp;
+    va_start(argp, sig);
+    jclass clazz = FindClassExtOrAbort(class_name);
+    CallStaticVoidMethodV(
+        clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp);
+    DeleteLocalRef(clazz);
+    va_end(argp);
+  }
+
+  void CallStaticVoidMethodOrAbort(const char* class_name,
+                                   const char* method_name,
+                                   const char* sig,
+                                   ...) {
+    va_list argp;
+    va_start(argp, sig);
+    jclass clazz = FindClassExtOrAbort(class_name);
+    CallStaticVoidMethodV(
+        clazz, GetStaticMethodIDOrAbort(clazz, method_name, sig), argp);
+    AbortOnException();
+    DeleteLocalRef(clazz);
+    va_end(argp);
+  }
+
+  jstring GetStringFieldOrAbort(jobject obj, const char* name) {
+    return static_cast<jstring>(
+        GetObjectFieldOrAbort(obj, name, "Ljava/lang/String;"));
+  }
+
+  jstring GetStaticStringFieldOrAbort(const char* class_name,
+                                      const char* name) {
+    return static_cast<jstring>(
+        GetStaticObjectFieldOrAbort(class_name, name, "Ljava/lang/String;"));
+  }
+
+  jstring GetStaticStringFieldOrAbort(jclass clazz, const char* name) {
+    return static_cast<jstring>(
+        GetStaticObjectFieldOrAbort(clazz, name, "Ljava/lang/String;"));
+  }
+
+// Convenience method to create a j[Type]Array from raw, native data. It is
+// the responsibility of clients to free the returned array when done with it
+// by manually calling |DeleteLocalRef| on it.
+#define X(_jtype, _jname)                                                   \
+  _jtype##Array New##_jname##ArrayFromRaw(const _jtype* data, jsize size) { \
+    SB_DCHECK(data);                                                        \
+    SB_DCHECK(size >= 0);                                                   \
+    _jtype##Array j_array = New##_jname##Array(size);                       \
+    SB_CHECK(j_array) << "Out of memory making new array";                  \
+    Set##_jname##ArrayRegion(j_array, 0, size, data);                       \
+    return j_array;                                                         \
+  }
+
+  X(jboolean, Boolean)
+  X(jbyte, Byte)
+  X(jchar, Char)
+  X(jshort, Short)
+  X(jint, Int)
+  X(jlong, Long)
+  X(jfloat, Float)
+  X(jdouble, Double)
+
+#undef X
+
+  jobject ConvertLocalRefToGlobalRef(jobject local) {
+    jobject global = NewGlobalRef(local);
+    DeleteLocalRef(local);
+    return global;
+  }
+
+  void AbortOnException() {
+    if (!ExceptionCheck()) {
+      return;
+    }
+    ExceptionDescribe();
+    SbSystemBreakIntoDebugger();
+  }
+};
+
+SB_COMPILE_ASSERT(sizeof(JNIEnv) == sizeof(JniEnvExt),
+                  JniEnvExt_must_not_add_fields);
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_JNI_ENV_EXT_H_
diff --git a/src/starboard/android/shared/jni_env_ext_test.cc b/src/starboard/android/shared/jni_env_ext_test.cc
new file mode 100644
index 0000000..4c69b95
--- /dev/null
+++ b/src/starboard/android/shared/jni_env_ext_test.cc
@@ -0,0 +1,102 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+// UTF-16, UTF-8, and Modified UTF-8 test strings, all "𐆖€£$"
+// 𐆖 U+10196 -> U16: D800 DD96  U8: F0 90 86 96  MU8: ED A0 80 ED B6 96
+// € U+020AC -> U16: 20AC       U8: E2 82 AC     MU8: E2 82 AC
+// £ U+000A3 -> U16: 00A3       U8: C2 A3        MU8: C2 A3
+// $ U+00024 -> U16: 0024       U8: 24           MU8: 24
+const char16_t kU16[] = u"\U00010196\u20AC\u00A3\u0024";
+const char kU8[] = "\xF0\x90\x86\x96\xE2\x82\xAC\xC2\xA3\x24";
+const char kMU8[] = "\xED\xA0\x80\xED\xB6\x96\xE2\x82\xAC\xC2\xA3\x24";
+
+// Subtract one from the array size to not count the null terminator.
+const int kU16Length = SB_ARRAY_SIZE(kU16) - 1;
+const int kU8Length = SB_ARRAY_SIZE(kU8) - 1;
+const int kMU8Length = SB_ARRAY_SIZE(kMU8) - 1;
+
+// Note: there is no test for getting the string back as modified UTF-8 since
+// on some Android devices GetStringUTFChars() may return standard UTF-8.
+// (e.g. Nexus Player returns modified UTF-8, but Shield returns standard UTF-8)
+// see: https://github.com/android-ndk/ndk/issues/283
+
+TEST(JniEnvExtTest, NewStringStandardUTF) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jstring j_str = env->NewStringStandardUTFOrAbort(kU8);
+
+  EXPECT_EQ(kU16Length, env->GetStringLength(j_str));
+  const jchar* u16_chars = env->GetStringChars(j_str, NULL);
+  std::u16string u16_string(
+      reinterpret_cast<const char16_t*>(u16_chars), kU16Length);
+  EXPECT_EQ(std::u16string(kU16), u16_string);
+  env->ReleaseStringChars(j_str, u16_chars);
+}
+
+TEST(JniEnvExtTest, NewStringModifiedUTF) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jstring j_str = env->NewStringUTF(kMU8);
+
+  EXPECT_EQ(kU16Length, env->GetStringLength(j_str));
+  const jchar* u16_chars = env->GetStringChars(j_str, NULL);
+  std::u16string u16_string(
+      reinterpret_cast<const char16_t*>(u16_chars), kU16Length);
+  EXPECT_EQ(std::u16string(kU16), u16_string);
+  env->ReleaseStringChars(j_str, u16_chars);
+}
+
+TEST(JniEnvExtTest, EmptyNewStringStandardUTF) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jstring j_str = env->NewStringStandardUTFOrAbort("");
+
+  EXPECT_EQ(0, env->GetStringLength(j_str));
+}
+
+TEST(JniEnvExtTest, GetStringStandardUTF) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jstring j_str =
+      env->NewString(reinterpret_cast<const jchar*>(kU16), kU16Length);
+
+  std::string str = env->GetStringStandardUTFOrAbort(j_str);
+  EXPECT_EQ(kU8Length, str.length());
+  EXPECT_EQ(std::string(kU8), str);
+  env->DeleteLocalRef(j_str);
+}
+
+TEST(JniEnvExtTest, EmptyGetStringStandardUTF) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jchar empty[] = {};
+  jstring j_str = env->NewString(empty, 0);
+
+  std::string str = env->GetStringStandardUTFOrAbort(j_str);
+  EXPECT_EQ(0, str.length());
+  EXPECT_EQ(std::string(), str);
+  env->DeleteLocalRef(j_str);
+}
+
+}  // namespace
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/jni_utils.h b/src/starboard/android/shared/jni_utils.h
new file mode 100644
index 0000000..0722504
--- /dev/null
+++ b/src/starboard/android/shared/jni_utils.h
@@ -0,0 +1,88 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
+#define STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
+
+#include <jni.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Wrapper class to manage the lifetime of a local reference to Java type
+// |JT|. This is necessary for local references to |JT|s that are obtained in
+// native code that was not called into from Java, since they will otherwise
+// not be cleaned up.
+template <typename JT>
+class ScopedLocalJavaRef {
+ public:
+  explicit ScopedLocalJavaRef(jobject j_object = NULL)
+      : jt_(static_cast<JT>(j_object)) {}
+  ~ScopedLocalJavaRef() {
+    if (jt_) {
+      JniEnvExt::Get()->DeleteLocalRef(jt_);
+      jt_ = NULL;
+    }
+  }
+  JT Get() const { return jt_; }
+  void Reset(jobject j_object) {
+    if (jt_) {
+      JniEnvExt::Get()->DeleteLocalRef(jt_);
+    }
+    jt_ = static_cast<JT>(j_object);
+  }
+  operator bool() const { return jt_; }
+
+ private:
+  JT jt_;
+
+  SB_DISALLOW_COPY_AND_ASSIGN(ScopedLocalJavaRef);
+};
+
+// Convenience class to manage the lifetime of a local Java ByteBuffer
+// reference, and provide accessors to its properties.
+class ScopedJavaByteBuffer {
+ public:
+  explicit ScopedJavaByteBuffer(jobject j_byte_buffer)
+      : j_byte_buffer_(j_byte_buffer) {}
+  void* address() const {
+    return JniEnvExt::Get()->GetDirectBufferAddress(j_byte_buffer_.Get());
+  }
+  jint capacity() const {
+    return JniEnvExt::Get()->GetDirectBufferCapacity(j_byte_buffer_.Get());
+  }
+  bool IsNull() const { return !j_byte_buffer_ || !address(); }
+  void CopyInto(const void* source, jint count) {
+    SB_DCHECK(!IsNull());
+    SB_DCHECK(count >= 0 && count <= capacity());
+    SbMemoryCopy(address(), source, count);
+  }
+
+ private:
+  ScopedLocalJavaRef<jobject> j_byte_buffer_;
+
+  SB_DISALLOW_COPY_AND_ASSIGN(ScopedJavaByteBuffer);
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_JNI_UTILS_H_
diff --git a/src/starboard/android/shared/launcher.py b/src/starboard/android/shared/launcher.py
new file mode 100644
index 0000000..bd43320
--- /dev/null
+++ b/src/starboard/android/shared/launcher.py
@@ -0,0 +1,396 @@
+#
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Android implementation of Starboard launcher abstraction."""
+
+import os
+import Queue
+import re
+import socket
+import subprocess
+import sys
+import threading
+import time
+
+import _env  # pylint: disable=unused-import
+
+from starboard.android.shared import sdk_utils
+from starboard.tools import abstract_launcher
+
+_APP_PACKAGE_NAME = 'dev.cobalt.coat'
+
+_APP_START_INTENT = 'dev.cobalt.coat/dev.cobalt.app.MainActivity'
+
+# Matches an "adb shell am monitor" error line.
+_RE_ADB_AM_MONITOR_ERROR = re.compile(r'\*\* ERROR')
+
+# String added to queue to indicate process has crashed
+_QUEUE_CODE_CRASHED = 'crashed'
+
+# Args to go/crow, which is started if no other device is attached.
+_CROW_COMMANDLINE = ['/google/data/ro/teams/mobile_eng_prod/crow/crow.par',
+                     '--api_level', '24', '--device', 'tv',
+                     '--open_gl_driver', 'host',
+                     '--noenable_g3_monitor']
+
+# How long to keep logging after a crash in order to emit the stack trace.
+_CRASH_LOG_SECONDS = 1.0
+
+_DEV_NULL = open('/dev/null')
+
+_ADB = os.path.join(sdk_utils.GetSdkPath(), 'platform-tools', 'adb')
+
+_RUNTIME_PERMISSIONS = [
+    'android.permission.GET_ACCOUNTS',
+    'android.permission.RECORD_AUDIO',
+]
+
+
+def TargetOsPathJoin(*path_elements):
+  """os.path.join for the target (Android)."""
+  return '/'.join(path_elements)
+
+
+def CleanLine(line):
+  """Removes trailing carriages returns from ADB output."""
+  return line.replace('\r', '')
+
+
+class StepTimer(object):
+  """Class for timing how long install/run steps take."""
+
+  def __init__(self, step_name):
+    self.step_name = step_name
+    self.start_time = time.time()
+    self.end_time = None
+
+  def Stop(self):
+    if self.start_time is None:
+      sys.stderr.write('Cannot stop timer; not started\n')
+    else:
+      self.end_time = time.time()
+      total_time = self.end_time - self.start_time
+      sys.stderr.write('Step \"{}\" took {} seconds.\n'.format(
+          self.step_name, total_time))
+
+
+class AdbCommandBuilder(object):
+  """Builder for 'adb' commands."""
+
+  def __init__(self, device_id):
+    self.device_id = device_id
+
+  def Build(self, *args):
+    """Builds an 'adb' commandline with the given args."""
+    result = [_ADB]
+    if self.device_id:
+      result.append('-s')
+      result.append(self.device_id)
+    result += list(args)
+    return result
+
+
+class AdbAmMonitorWatcher(object):
+  """Watches an "adb shell am monitor" process to detect crashes."""
+
+  def __init__(self, adb_builder, done_queue):
+    self.adb_builder = adb_builder
+    self.process = subprocess.Popen(
+        adb_builder.Build('shell', 'am', 'monitor'),
+        stdout=subprocess.PIPE,
+        stderr=_DEV_NULL,
+        close_fds=True)
+    self.thread = threading.Thread(target=self._Run)
+    self.thread.start()
+    self.done_queue = done_queue
+
+  def Shutdown(self):
+    self.process.kill()
+    self.thread.join()
+
+  def _Run(self):
+    while True:
+      line = CleanLine(self.process.stdout.readline())
+      if not line:
+        return
+      if re.search(_RE_ADB_AM_MONITOR_ERROR, line):
+        self.done_queue.put(_QUEUE_CODE_CRASHED)
+        # This log line will wake up the main thread
+        subprocess.call(
+            self.adb_builder.Build('shell', 'log', '-t', 'starboard',
+                                   'am monitor detected crash'),
+            close_fds=True)
+
+
+class Launcher(abstract_launcher.AbstractLauncher):
+  """Run an application on Android."""
+
+  def __init__(self, platform, target_name, config, device_id, **kwargs):
+
+    super(Launcher, self).__init__(platform, target_name, config, device_id,
+                                   **kwargs)
+
+    if not self.device_id:
+      self.device_id = self._IdentifyDevice()
+    else:
+      self._ConnectIfNecessary()
+
+    self.adb_builder = AdbCommandBuilder(self.device_id)
+
+    out_directory = os.path.split(self.GetTargetPath())[0]
+    self.apk_path = os.path.join(out_directory, '{}.apk'.format(target_name))
+    if not os.path.exists(self.apk_path):
+      raise Exception("Can't find APK {}".format(self.apk_path))
+
+    # This flag is set when the main Run() loop exits.  If Kill() is called
+    # after this flag is set, it will not do anything.
+    self.killed = threading.Event()
+
+    # Keep track of the port used by ADB forward in order to remove it later
+    # on.
+    self.local_port = None
+
+  def _IsValidIPv4Address(self, address):
+    """Returns True if address is a valid IPv4 address, False otherwise."""
+    try:
+      # inet_aton throws an exception if the address is not a valid IPv4
+      # address. However addresses such as '127.1' might still be considered
+      # valid, hence the check for 3 '.' in the address.
+      if socket.inet_aton(address) and address.count('.') == 3:
+        return True
+    except:
+      pass
+    return False
+
+  def _GetAdbDevices(self):
+    """Returns a list of names of connected devices, or empty list if none."""
+
+    # Does not use the ADBCommandBuilder class because this command should be
+    # run without targeting a specific device.
+    p = subprocess.Popen([_ADB, 'devices'], stderr=_DEV_NULL,
+                         stdout=subprocess.PIPE, close_fds=True)
+    result = p.stdout.readlines()[1:-1]
+    p.wait()
+
+    names = []
+    for device in result:
+      name_info = device.split('\t')
+      # Some devices may not have authorization for USB debugging.
+      try:
+        if 'unauthorized' not in name_info[1]:
+          names.append(name_info[0])
+      # Sometimes happens when device is found, even though none are connected.
+      except IndexError:
+        continue
+    return names
+
+  def _IdentifyDevice(self):
+    """Picks a device to be used to run the executable.
+
+    In the event that no device_id is provided, but multiple
+    devices are connected, this method chooses the first device
+    listed.
+
+    Returns:
+      The name of an attached device, or None if no devices are present.
+    """
+    device_name = None
+
+    devices = self._GetAdbDevices()
+    if devices:
+      device_name = devices[0]
+
+    return device_name
+
+  def _ConnectIfNecessary(self):
+    """Run ADB connect if needed for devices connected over IP."""
+    if not self._IsValidIPv4Address(self.device_id):
+      return
+    for device in self._GetAdbDevices():
+      # Devices returned by _GetAdbDevices might include port number, so cannot
+      # simply check if self.device_id is in the returned list.
+      if self.device_id in device:
+        return
+
+    # Device isn't connected. Run ADB connect.
+    # Does not use the ADBCommandBuilder class because this command should be
+    # run without targeting a specific device.
+    p = subprocess.Popen([_ADB, 'connect', self.device_id], stderr=_DEV_NULL,
+                         stdout=subprocess.PIPE, close_fds=True)
+    result = p.stdout.readlines()[0]
+    p.wait()
+
+    if 'connected to' not in result:
+      sys.stderr.write('Failed to connect to {}\n'.format(self.device_id))
+
+  def _LaunchCrowIfNecessary(self):
+    if self.device_id:
+      return
+
+    # Note that we just leave Crow running, since we uninstall/reinstall
+    # each time anyway.
+    self._CheckCall(*_CROW_COMMANDLINE)
+
+  def _Call(self, *args):
+    sys.stderr.write('{}\n'.format(' '.join(args)))
+    subprocess.call(args, stdout=_DEV_NULL, stderr=_DEV_NULL,
+                    close_fds=True)
+
+  def _CallAdb(self, *in_args):
+    args = self.adb_builder.Build(*in_args)
+    self._Call(*args)
+
+  def _CheckCall(self, *args):
+    sys.stderr.write('{}\n'.format(' '.join(args)))
+    subprocess.check_call(args, stdout=_DEV_NULL, stderr=_DEV_NULL,
+                          close_fds=True)
+
+  def _CheckCallAdb(self, *in_args):
+    args = self.adb_builder.Build(*in_args)
+    self._CheckCall(*args)
+
+  def _PopenAdb(self, *args, **kwargs):
+    return subprocess.Popen(self.adb_builder.Build(*args), close_fds=True,
+                            **kwargs)
+
+  def Run(self):
+    # The return code for binaries run on Android is read from a log line that
+    # it emitted in android_main.cc.  This return_code variable will be assigned
+    # the value read when we see that line, or left at 1 in the event of a crash
+    # or early exit.
+    return_code = 1
+
+    # Setup for running executable
+    self._LaunchCrowIfNecessary()
+    self._CheckCallAdb('wait-for-device')
+    self._Shutdown()
+
+    # Clear logcat
+    self._CheckCallAdb('logcat', '-c')
+
+    # Install the APK.
+    install_timer = StepTimer('install')
+    self._CheckCallAdb('install', '-r', self.apk_path)
+    install_timer.Stop()
+
+    # Send the wakeup key to ensure daydream isn't running, otherwise Activity
+    # Manager may get in a loop running the test over and over again.
+    self._CheckCallAdb('shell', 'input', 'keyevent', 'KEY_WAKEUP')
+
+    # Grant runtime permissions to avoid prompts during testing.
+    for permission in _RUNTIME_PERMISSIONS:
+      self._CheckCallAdb('shell', 'pm', 'grant', _APP_PACKAGE_NAME, permission)
+
+    done_queue = Queue.Queue()
+    am_monitor = AdbAmMonitorWatcher(self.adb_builder, done_queue)
+
+    # Increases the size of the logcat buffer.  Without this, the log buffer
+    # will not flush quickly enough and output will be cut off.
+    self._CheckCallAdb('logcat', '-G', '2M')
+
+    #  Ctrl + C will kill this process
+    logcat_process = self._PopenAdb(
+        'logcat', '-v', 'raw', '-s', '*:F', 'DEBUG:*', 'System.err:*',
+        'starboard:*', 'starboard_media:*',
+        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+    # Actually running executable
+    run_timer = StepTimer('running executable')
+    try:
+      args = ['shell', 'am', 'start']
+      command_line_params = [
+          '--android_log_sleep_time=1000',
+          '--disable_sign_in',
+      ]
+      if self.target_command_line_params:
+        command_line_params += self.target_command_line_params
+      args += ['--esa', 'args', "'{}'".format(','.join(command_line_params))]
+      args += [_APP_START_INTENT]
+
+      self._CheckCallAdb(*args)
+
+      app_crashed = False
+      while True:
+        if not done_queue.empty():
+          done_queue_code = done_queue.get_nowait()
+          if done_queue_code == _QUEUE_CODE_CRASHED:
+            app_crashed = True
+            threading.Timer(_CRASH_LOG_SECONDS, logcat_process.kill).start()
+
+        # Note we cannot use "for line in logcat_process.stdout" because
+        # that uses a large buffer which will cause us to deadlock.
+        line = CleanLine(logcat_process.stdout.readline())
+
+        # Some crashes are not caught by the am_monitor thread, but they do
+        # produce the following string in logcat before they exit.
+        if 'beginning of crash' in line:
+          app_crashed = True
+          threading.Timer(_CRASH_LOG_SECONDS, logcat_process.kill).start()
+
+        if not line:  # Logcat exited, or was killed
+          break
+        else:
+          self._WriteLine(line)
+          # Don't break until we see the below text in logcat, which should be
+          # written when the Starboard application event loop finishes.
+          if '***Application Stopped***' in line:
+            try:
+              return_code = int(line.split(' ')[-1])
+            except ValueError:  # Error message was printed to stdout
+              pass
+            logcat_process.kill()
+            break
+
+    finally:
+      if app_crashed:
+        self._WriteLine('***Application Crashed***\n')
+      else:
+        self._Shutdown()
+      if self.local_port is not None:
+        self._CallAdb('forward', '--remove', 'tcp:{}'.format(self.local_port))
+      am_monitor.Shutdown()
+      self.killed.set()
+      run_timer.Stop()
+
+    return return_code
+
+  def _Shutdown(self):
+    self._CallAdb('shell', 'am', 'force-stop', _APP_PACKAGE_NAME)
+
+  def Kill(self):
+    if not self.killed.is_set():
+      sys.stderr.write('***Killing Launcher***\n')
+      self._CheckCallAdb('shell', 'log', '-t', 'starboard',
+                         '***Application Stopped*** 1')
+      self._Shutdown()
+    else:
+      sys.stderr.write('Cannot kill launcher: already dead.\n')
+
+  def _WriteLine(self, line):
+    """Write log output to stdout."""
+    self.output_file.write(line)
+    self.output_file.flush()
+
+  def GetHostAndPortGivenPort(self, port):
+    forward_p = self._PopenAdb(
+        'forward', 'tcp:0', 'tcp:{}'.format(port),
+        stdout=subprocess.PIPE,
+        stderr=_DEV_NULL)
+    forward_p.wait()
+
+    self.local_port = CleanLine(forward_p.stdout.readline()).rstrip('\n')
+    sys.stderr.write('ADB forward local port {} '
+                     '=> device port {}\n'.format(self.local_port, port))
+    return socket.gethostbyname('localhost'), self.local_port
diff --git a/src/starboard/android/shared/log.cc b/src/starboard/android/shared/log.cc
new file mode 100644
index 0000000..a202f71
--- /dev/null
+++ b/src/starboard/android/shared/log.cc
@@ -0,0 +1,103 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android/log.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <jni.h>
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/log_internal.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/command_line.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+  const char kLogSleepTimeSwitch[] = "android_log_sleep_time";
+  SbTime g_log_sleep_time = 0;
+}
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+void LogInit(const starboard::shared::starboard::CommandLine& command_line) {
+  if (command_line.HasSwitch(kLogSleepTimeSwitch)) {
+    g_log_sleep_time =
+        SbStringAToL(command_line.GetSwitchValue(kLogSleepTimeSwitch).c_str());
+    SB_LOG(INFO) << "Android log sleep time: " << g_log_sleep_time;
+  }
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+void SbLog(SbLogPriority priority, const char* message) {
+  int android_priority;
+  switch (priority) {
+    case kSbLogPriorityUnknown:
+      android_priority = ANDROID_LOG_UNKNOWN;
+      break;
+    case kSbLogPriorityInfo:
+      android_priority = ANDROID_LOG_INFO;
+      break;
+    case kSbLogPriorityWarning:
+      android_priority = ANDROID_LOG_WARN;
+      break;
+    case kSbLogPriorityError:
+      android_priority = ANDROID_LOG_ERROR;
+      break;
+    case kSbLogPriorityFatal:
+      android_priority = ANDROID_LOG_FATAL;
+      break;
+    default:
+      android_priority = ANDROID_LOG_INFO;
+      break;
+  }
+  __android_log_write(android_priority, "starboard", message);
+
+  // In unit tests the logging is too fast for the android log to be read out
+  // and we end up losing crucial logs. The test runner specifies a sleep time.
+  SbThreadSleep(g_log_sleep_time);
+}
+
+// Helper to write messages to logcat even when Android non-warning/non-error
+// logging is stripped from the app with Proguard.
+extern "C" SB_EXPORT_PLATFORM jint
+Java_dev_cobalt_util_Log_nativeWrite(JniEnvExt* env,
+                                     jobject unused_clazz,
+                                     jchar priority,
+                                     jstring tag,
+                                     jstring msg,
+                                     jobject throwable) {
+  char log_method_name[2] = {static_cast<char>(priority), '\0'};
+  if (throwable == nullptr) {
+    return env->CallStaticIntMethodOrAbort(
+        "android.util.Log", log_method_name,
+        "(Ljava/lang/String;Ljava/lang/String;)I", tag, msg);
+  } else {
+    return env->CallStaticIntMethodOrAbort(
+        "android.util.Log", log_method_name,
+        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I",
+        tag, msg, throwable);
+  }
+}
diff --git a/src/starboard/android/shared/log_flush.cc b/src/starboard/android/shared/log_flush.cc
new file mode 100644
index 0000000..09d54e2
--- /dev/null
+++ b/src/starboard/android/shared/log_flush.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+void SbLogFlush() {
+}
diff --git a/src/starboard/android/shared/log_format.cc b/src/starboard/android/shared/log_format.cc
new file mode 100644
index 0000000..734d22f
--- /dev/null
+++ b/src/starboard/android/shared/log_format.cc
@@ -0,0 +1,54 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <stdio.h>
+#include <string>
+
+#include "starboard/log.h"
+#include "starboard/mutex.h"
+#include "starboard/string.h"
+
+namespace {
+SbMutex log_line_mutex = SB_MUTEX_INITIALIZER;
+std::stringstream log_line;
+const int kFormatBufferSizeBytes = 16 * 1024;
+
+}  // namespace
+
+void SbLogFormat(const char* format, va_list arguments) {
+  // __android_log_vprint() cannot be used here because each call to it
+  // will produce a new line in the log, whereas a call to a function in
+  // the C vprintf family only produces a new line if fed "\n", etc.  The logic
+  // below ensures that a log line is written only when a newline is detected,
+  // making Android SbLogFormat() behavior consistent with the expectations of
+  // the code that uses it, such as unit test suites.
+
+  char formatted_buffer[kFormatBufferSizeBytes];
+  vsprintf(formatted_buffer, format, arguments);
+
+  const char* newline = SbStringFindCharacter(formatted_buffer, '\n');
+
+  SbMutexAcquire(&log_line_mutex);
+  std::string buffer_string(formatted_buffer);
+  log_line << buffer_string;
+  if (newline != NULL) {
+    log_line.flush();
+
+    SbLogRaw(log_line.str().c_str());
+
+    log_line.str("");
+    log_line.clear();
+  }
+  SbMutexRelease(&log_line_mutex);
+}
diff --git a/src/starboard/android/shared/log_internal.h b/src/starboard/android/shared/log_internal.h
new file mode 100644
index 0000000..5fb5169
--- /dev/null
+++ b/src/starboard/android/shared/log_internal.h
@@ -0,0 +1,33 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This header provides a mechanism for multiple Android logging
+// formats to share a single log file handle.
+
+#ifndef STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
+
+#include "starboard/shared/starboard/command_line.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+void LogInit(const starboard::shared::starboard::CommandLine& command_line);
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_LOG_INTERNAL_H_
diff --git a/src/starboard/android/shared/log_is_tty.cc b/src/starboard/android/shared/log_is_tty.cc
new file mode 100644
index 0000000..fba57de
--- /dev/null
+++ b/src/starboard/android/shared/log_is_tty.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+#include <unistd.h>
+
+bool SbLogIsTty() {
+  return false;
+}
diff --git a/src/starboard/android/shared/log_raw.cc b/src/starboard/android/shared/log_raw.cc
new file mode 100644
index 0000000..79d63ad
--- /dev/null
+++ b/src/starboard/android/shared/log_raw.cc
@@ -0,0 +1,19 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/log.h"
+
+void SbLogRaw(const char* message) {
+  SbLog(kSbLogPriorityInfo, message);
+}
diff --git a/src/starboard/android/shared/main.cc b/src/starboard/android/shared/main.cc
new file mode 100644
index 0000000..5769390
--- /dev/null
+++ b/src/starboard/android/shared/main.cc
@@ -0,0 +1,23 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/export.h"
+#include "starboard/log.h"
+
+extern "C" SB_EXPORT_PLATFORM int main(int argc, char** argv) {
+  // main() is never called on Android. However, the cobalt_bin
+  // target requires it to be there.
+  SB_NOTREACHED();
+  return 0;
+}
diff --git a/src/starboard/android/shared/media_codec_bridge.cc b/src/starboard/android/shared/media_codec_bridge.cc
new file mode 100644
index 0000000..4ac70c0
--- /dev/null
+++ b/src/starboard/android/shared/media_codec_bridge.cc
@@ -0,0 +1,358 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/media_codec_bridge.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+// See
+// https://developer.android.com/reference/android/media/MediaFormat.html#COLOR_RANGE_FULL.
+const jint COLOR_RANGE_FULL = 1;
+const jint COLOR_RANGE_LIMITED = 2;
+
+const jint COLOR_STANDARD_BT2020 = 6;
+const jint COLOR_STANDARD_BT601_NTSC = 4;
+const jint COLOR_STANDARD_BT601_PAL = 2;
+const jint COLOR_STANDARD_BT709 = 1;
+
+const jint COLOR_TRANSFER_HLG = 7;
+const jint COLOR_TRANSFER_LINEAR = 1;
+const jint COLOR_TRANSFER_SDR_VIDEO = 3;
+const jint COLOR_TRANSFER_ST2084 = 6;
+
+// A special value to represent that no mapping between an SbMedia* HDR
+// metadata value and Android HDR metadata value is possible.  This value
+// implies that HDR playback should not be attempted.
+const jint COLOR_VALUE_UNKNOWN = -1;
+
+jint SbMediaPrimaryIdToColorStandard(SbMediaPrimaryId primary_id) {
+  switch (primary_id) {
+    case kSbMediaPrimaryIdBt709:
+      return COLOR_STANDARD_BT709;
+    case kSbMediaPrimaryIdBt2020:
+      return COLOR_STANDARD_BT2020;
+    default:
+      return COLOR_VALUE_UNKNOWN;
+  }
+}
+
+jint SbMediaTransferIdToColorTransfer(SbMediaTransferId transfer_id) {
+  switch (transfer_id) {
+    case kSbMediaTransferIdBt709:
+      return COLOR_TRANSFER_SDR_VIDEO;
+    case kSbMediaTransferIdSmpteSt2084:
+      return COLOR_TRANSFER_ST2084;
+    case kSbMediaTransferIdAribStdB67:
+      return COLOR_TRANSFER_HLG;
+    default:
+      return COLOR_VALUE_UNKNOWN;
+  }
+}
+
+jint SbMediaRangeIdToColorRange(SbMediaRangeId range_id) {
+  switch (range_id) {
+    case kSbMediaRangeIdLimited:
+      return COLOR_RANGE_LIMITED;
+    case kSbMediaRangeIdFull:
+      return COLOR_RANGE_FULL;
+    default:
+      return COLOR_VALUE_UNKNOWN;
+  }
+}
+
+}  // namespace
+
+// static
+scoped_ptr<MediaCodecBridge> MediaCodecBridge::CreateAudioMediaCodecBridge(
+    SbMediaAudioCodec audio_codec,
+    const SbMediaAudioHeader& audio_header,
+    jobject j_media_crypto) {
+  const char* mime = SupportedAudioCodecToMimeType(audio_codec);
+  if (!mime) {
+    return scoped_ptr<MediaCodecBridge>(NULL);
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+  jobject j_media_codec_bridge = env->CallStaticObjectMethodOrAbort(
+      "dev/cobalt/media/MediaCodecBridge", "createAudioMediaCodecBridge",
+      "(Ljava/lang/String;ZZIILandroid/media/MediaCrypto;)Ldev/cobalt/media/"
+      "MediaCodecBridge;",
+      j_mime.Get(), !!j_media_crypto, false, audio_header.samples_per_second,
+      audio_header.number_of_channels, j_media_crypto);
+
+  if (!j_media_codec_bridge) {
+    return scoped_ptr<MediaCodecBridge>(NULL);
+  }
+
+  j_media_codec_bridge = env->ConvertLocalRefToGlobalRef(j_media_codec_bridge);
+  return scoped_ptr<MediaCodecBridge>(
+      new MediaCodecBridge(j_media_codec_bridge));
+}
+
+// static
+scoped_ptr<MediaCodecBridge> MediaCodecBridge::CreateVideoMediaCodecBridge(
+    SbMediaVideoCodec video_codec,
+    int width,
+    int height,
+    jobject j_surface,
+    jobject j_media_crypto,
+    const SbMediaColorMetadata* color_metadata) {
+  const char* mime = SupportedVideoCodecToMimeType(video_codec);
+  if (!mime) {
+    return scoped_ptr<MediaCodecBridge>(NULL);
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+
+  ScopedLocalJavaRef<jobject> j_color_info(nullptr);
+  if (color_metadata) {
+    jint color_standard =
+        SbMediaPrimaryIdToColorStandard(color_metadata->primaries);
+    jint color_transfer =
+        SbMediaTransferIdToColorTransfer(color_metadata->transfer);
+    jint color_range = SbMediaRangeIdToColorRange(color_metadata->range);
+
+    if (color_standard != COLOR_VALUE_UNKNOWN &&
+        color_transfer != COLOR_VALUE_UNKNOWN &&
+        color_range != COLOR_VALUE_UNKNOWN) {
+      const auto& mastering_metadata = color_metadata->mastering_metadata;
+      j_color_info.Reset(env->NewObjectOrAbort(
+          "dev/cobalt/media/MediaCodecBridge$ColorInfo", "(IIIFFFFFFFFFF)V",
+          color_range, color_standard, color_transfer,
+          mastering_metadata.primary_r_chromaticity_x,
+          mastering_metadata.primary_r_chromaticity_y,
+          mastering_metadata.primary_g_chromaticity_x,
+          mastering_metadata.primary_g_chromaticity_y,
+          mastering_metadata.primary_b_chromaticity_x,
+          mastering_metadata.primary_b_chromaticity_y,
+          mastering_metadata.white_point_chromaticity_x,
+          mastering_metadata.white_point_chromaticity_y,
+          mastering_metadata.luminance_max, mastering_metadata.luminance_min));
+    }
+  }
+
+  jobject j_media_codec_bridge = env->CallStaticObjectMethodOrAbort(
+      "dev/cobalt/media/MediaCodecBridge", "createVideoMediaCodecBridge",
+      "(Ljava/lang/String;ZZIILandroid/view/Surface;"
+      "Landroid/media/MediaCrypto;"
+      "Ldev/cobalt/media/MediaCodecBridge$ColorInfo;)"
+      "Ldev/cobalt/media/MediaCodecBridge;",
+      j_mime.Get(), !!j_media_crypto, false, width, height, j_surface,
+      j_media_crypto, j_color_info.Get());
+
+  if (!j_media_codec_bridge) {
+    return scoped_ptr<MediaCodecBridge>(NULL);
+  }
+
+  j_media_codec_bridge = env->ConvertLocalRefToGlobalRef(j_media_codec_bridge);
+  return scoped_ptr<MediaCodecBridge>(
+      new MediaCodecBridge(j_media_codec_bridge));
+}
+
+MediaCodecBridge::~MediaCodecBridge() {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  SB_DCHECK(j_media_codec_bridge_);
+  env->CallVoidMethodOrAbort(j_media_codec_bridge_, "release", "()V");
+  env->DeleteGlobalRef(j_media_codec_bridge_);
+  j_media_codec_bridge_ = NULL;
+
+  SB_DCHECK(j_reused_dequeue_input_result_);
+  env->DeleteGlobalRef(j_reused_dequeue_input_result_);
+  j_reused_dequeue_input_result_ = NULL;
+
+  SB_DCHECK(j_reused_dequeue_output_result_);
+  env->DeleteGlobalRef(j_reused_dequeue_output_result_);
+  j_reused_dequeue_output_result_ = NULL;
+
+  SB_DCHECK(j_reused_get_output_format_result_);
+  env->DeleteGlobalRef(j_reused_get_output_format_result_);
+  j_reused_get_output_format_result_ = NULL;
+}
+
+DequeueInputResult MediaCodecBridge::DequeueInputBuffer(jlong timeout_us) {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(
+      j_media_codec_bridge_, "dequeueInputBuffer",
+      "(JLdev/cobalt/media/MediaCodecBridge$DequeueInputResult;)V", timeout_us,
+      j_reused_dequeue_input_result_);
+  return {env->CallIntMethodOrAbort(j_reused_dequeue_input_result_, "status",
+                                    "()I"),
+          env->CallIntMethodOrAbort(j_reused_dequeue_input_result_, "index",
+                                    "()I")};
+}
+
+jobject MediaCodecBridge::GetInputBuffer(jint index) {
+  SB_DCHECK(index >= 0);
+  return JniEnvExt::Get()->CallObjectMethodOrAbort(
+      j_media_codec_bridge_, "getInputBuffer", "(I)Ljava/nio/ByteBuffer;",
+      index);
+}
+
+jint MediaCodecBridge::QueueInputBuffer(jint index,
+                                        jint offset,
+                                        jint size,
+                                        jlong presentation_time_microseconds,
+                                        jint flags) {
+  return JniEnvExt::Get()->CallIntMethodOrAbort(
+      j_media_codec_bridge_, "queueInputBuffer", "(IIIJI)I", index, offset,
+      size, presentation_time_microseconds, flags);
+}
+
+jint MediaCodecBridge::QueueSecureInputBuffer(
+    jint index,
+    jint offset,
+    const SbDrmSampleInfo& drm_sample_info,
+    jlong presentation_time_microseconds) {
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jbyteArray> j_iv(env->NewByteArrayFromRaw(
+      reinterpret_cast<const jbyte*>(drm_sample_info.initialization_vector),
+      drm_sample_info.initialization_vector_size));
+  ScopedLocalJavaRef<jbyteArray> j_key_id(env->NewByteArrayFromRaw(
+      reinterpret_cast<const jbyte*>(drm_sample_info.identifier),
+      drm_sample_info.identifier_size));
+
+  // Reshape the sub sample mapping like this:
+  // [(c0, e0), (c1, e1), ...] -> [c0, c1, ...] and [e0, e1, ...]
+  int32_t subsample_count = drm_sample_info.subsample_count;
+  scoped_array<jint> clear_bytes(new jint[subsample_count]);
+  scoped_array<jint> encrypted_bytes(new jint[subsample_count]);
+  for (int i = 0; i < subsample_count; ++i) {
+    clear_bytes[i] = drm_sample_info.subsample_mapping[i].clear_byte_count;
+    encrypted_bytes[i] =
+        drm_sample_info.subsample_mapping[i].encrypted_byte_count;
+  }
+  ScopedLocalJavaRef<jintArray> j_clear_bytes(
+      env->NewIntArrayFromRaw(clear_bytes.get(), subsample_count));
+  ScopedLocalJavaRef<jintArray> j_encrypted_bytes(
+      env->NewIntArrayFromRaw(encrypted_bytes.get(), subsample_count));
+
+  return env->CallIntMethodOrAbort(
+      j_media_codec_bridge_, "queueSecureInputBuffer", "(II[B[B[I[IIIIIJ)I",
+      index, offset, j_iv.Get(), j_key_id.Get(), j_clear_bytes.Get(),
+      j_encrypted_bytes.Get(), subsample_count, CRYPTO_MODE_AES_CTR, 0, 0,
+      presentation_time_microseconds);
+}
+
+DequeueOutputResult MediaCodecBridge::DequeueOutputBuffer(jlong timeout_us) {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(
+      j_media_codec_bridge_, "dequeueOutputBuffer",
+      "(JLdev/cobalt/media/MediaCodecBridge$DequeueOutputResult;)V", timeout_us,
+      j_reused_dequeue_output_result_);
+  return {env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "status",
+                                    "()I"),
+          env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "index",
+                                    "()I"),
+          env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "flags",
+                                    "()I"),
+          env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "offset",
+                                    "()I"),
+          env->CallLongMethodOrAbort(j_reused_dequeue_output_result_,
+                                     "presentationTimeMicroseconds", "()J"),
+          env->CallIntMethodOrAbort(j_reused_dequeue_output_result_, "numBytes",
+                                    "()I")};
+}
+
+jobject MediaCodecBridge::GetOutputBuffer(jint index) {
+  SB_DCHECK(index >= 0);
+  return JniEnvExt::Get()->CallObjectMethodOrAbort(
+      j_media_codec_bridge_, "getOutputBuffer", "(I)Ljava/nio/ByteBuffer;",
+      index);
+}
+
+void MediaCodecBridge::ReleaseOutputBuffer(jint index, jboolean render) {
+  JniEnvExt::Get()->CallVoidMethodOrAbort(
+      j_media_codec_bridge_, "releaseOutputBuffer", "(IZ)V", index, render);
+}
+
+void MediaCodecBridge::ReleaseOutputBufferAtTimestamp(
+    jint index,
+    jlong render_timestamp_ns) {
+  JniEnvExt::Get()->CallVoidMethodOrAbort(j_media_codec_bridge_,
+                                          "releaseOutputBuffer", "(IJ)V", index,
+                                          render_timestamp_ns);
+}
+
+jint MediaCodecBridge::Flush() {
+  return JniEnvExt::Get()->CallIntMethodOrAbort(j_media_codec_bridge_, "flush",
+                                                "()I");
+}
+
+SurfaceDimensions MediaCodecBridge::GetOutputDimensions() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(
+      j_media_codec_bridge_, "getOutputFormat",
+      "(Ldev/cobalt/media/MediaCodecBridge$GetOutputFormatResult;)V",
+      j_reused_get_output_format_result_);
+  return {env->CallIntMethodOrAbort(j_reused_get_output_format_result_, "width",
+                                    "()I"),
+          env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+                                    "height", "()I")};
+}
+
+AudioOutputFormatResult MediaCodecBridge::GetAudioOutputFormat() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(
+      j_media_codec_bridge_, "getOutputFormat",
+      "(Ldev/cobalt/media/MediaCodecBridge$GetOutputFormatResult;)V",
+      j_reused_get_output_format_result_);
+
+  jint status = env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+                                          "status", "()I");
+  if (status == MEDIA_CODEC_ERROR) {
+    return {status, 0, 0};
+  }
+
+  return {status, env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+                                            "sampleRate", "()I"),
+          env->CallIntMethodOrAbort(j_reused_get_output_format_result_,
+                                    "channelCount", "()I")};
+}
+
+MediaCodecBridge::MediaCodecBridge(jobject j_media_codec_bridge)
+    : j_media_codec_bridge_(j_media_codec_bridge),
+      j_reused_dequeue_input_result_(NULL),
+      j_reused_dequeue_output_result_(NULL),
+      j_reused_get_output_format_result_(NULL) {
+  SB_DCHECK(j_media_codec_bridge_);
+  JniEnvExt* env = JniEnvExt::Get();
+  SB_DCHECK(env->GetObjectRefType(j_media_codec_bridge_) == JNIGlobalRefType);
+
+  j_reused_dequeue_input_result_ = env->NewObjectOrAbort(
+      "dev/cobalt/media/MediaCodecBridge$DequeueInputResult", "()V");
+  SB_DCHECK(j_reused_dequeue_input_result_);
+  j_reused_dequeue_input_result_ =
+      env->ConvertLocalRefToGlobalRef(j_reused_dequeue_input_result_);
+
+  j_reused_dequeue_output_result_ = env->NewObjectOrAbort(
+      "dev/cobalt/media/MediaCodecBridge$DequeueOutputResult", "()V");
+  SB_DCHECK(j_reused_dequeue_output_result_);
+  j_reused_dequeue_output_result_ =
+      env->ConvertLocalRefToGlobalRef(j_reused_dequeue_output_result_);
+
+  j_reused_get_output_format_result_ = env->NewObjectOrAbort(
+      "dev/cobalt/media/MediaCodecBridge$GetOutputFormatResult", "()V");
+  SB_DCHECK(j_reused_get_output_format_result_);
+  j_reused_get_output_format_result_ =
+      env->ConvertLocalRefToGlobalRef(j_reused_get_output_format_result_);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/media_codec_bridge.h b/src/starboard/android/shared/media_codec_bridge.h
new file mode 100644
index 0000000..3172e33
--- /dev/null
+++ b/src/starboard/android/shared/media_codec_bridge.h
@@ -0,0 +1,139 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/common/scoped_ptr.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// These must be in sync with MediaCodecWrapper.MEDIA_CODEC_XXX constants in
+// MediaCodecBridge.java.
+const jint MEDIA_CODEC_OK = 0;
+const jint MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER = 1;
+const jint MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER = 2;
+const jint MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED = 3;
+const jint MEDIA_CODEC_OUTPUT_FORMAT_CHANGED = 4;
+const jint MEDIA_CODEC_INPUT_END_OF_STREAM = 5;
+const jint MEDIA_CODEC_OUTPUT_END_OF_STREAM = 6;
+const jint MEDIA_CODEC_NO_KEY = 7;
+const jint MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION = 8;
+const jint MEDIA_CODEC_ABORT = 9;
+const jint MEDIA_CODEC_ERROR = 10;
+
+const jint BUFFER_FLAG_CODEC_CONFIG = 2;
+const jint BUFFER_FLAG_END_OF_STREAM = 4;
+
+const jint CRYPTO_MODE_UNENCRYPTED = 0;
+const jint CRYPTO_MODE_AES_CTR = 1;
+const jint CRYPTO_MODE_AES_CBC = 2;
+
+struct DequeueInputResult {
+  jint status;
+  jint index;
+};
+
+struct DequeueOutputResult {
+  jint status;
+  jint index;
+  jint flags;
+  jint offset;
+  jlong presentation_time_microseconds;
+  jint num_bytes;
+};
+
+struct SurfaceDimensions {
+  jint width;
+  jint height;
+};
+
+struct AudioOutputFormatResult {
+  jint status;
+  jint sample_rate;
+  jint channel_count;
+};
+
+class MediaCodecBridge {
+ public:
+  static scoped_ptr<MediaCodecBridge> CreateAudioMediaCodecBridge(
+      SbMediaAudioCodec audio_codec,
+      const SbMediaAudioHeader& audio_header,
+      jobject j_media_crypto);
+
+  static scoped_ptr<MediaCodecBridge> CreateVideoMediaCodecBridge(
+      SbMediaVideoCodec video_codec,
+      int width,
+      int height,
+      jobject j_surface,
+      jobject j_media_crypto,
+      const SbMediaColorMetadata* color_metadata);
+
+  ~MediaCodecBridge();
+
+  DequeueInputResult DequeueInputBuffer(jlong timeout_us);
+  // It is the responsibility of the client to manage the lifetime of the
+  // jobject that |GetInputBuffer| returns.
+  jobject GetInputBuffer(jint index);
+  jint QueueInputBuffer(jint index,
+                        jint offset,
+                        jint size,
+                        jlong presentation_time_microseconds,
+                        jint flags);
+  jint QueueSecureInputBuffer(jint index,
+                              jint offset,
+                              const SbDrmSampleInfo& drm_sample_info,
+                              jlong presentation_time_microseconds);
+
+  DequeueOutputResult DequeueOutputBuffer(jlong timeout_us);
+  // It is the responsibility of the client to manage the lifetime of the
+  // jobject that |GetOutputBuffer| returns.
+  jobject GetOutputBuffer(jint index);
+  void ReleaseOutputBuffer(jint index, jboolean render);
+  void ReleaseOutputBufferAtTimestamp(jint index, jlong render_timestamp_ns);
+
+  jint Flush();
+  SurfaceDimensions GetOutputDimensions();
+  AudioOutputFormatResult GetAudioOutputFormat();
+
+ private:
+  // |MediaCodecBridge|s must only be created through its factory methods.
+  explicit MediaCodecBridge(jobject j_media_codec_bridge);
+
+  jobject j_media_codec_bridge_;
+
+  // Profiling and allocation tracking has identified this area to be hot,
+  // and, capable of enough to cause GC times to raise high enough to impact
+  // playback.  We mitigate this by reusing these output objects between calls
+  // to |DequeueInputBuffer|, |DequeueOutputBuffer|, and
+  // |GetOutputDimensions|.
+  jobject j_reused_dequeue_input_result_;
+  jobject j_reused_dequeue_output_result_;
+  jobject j_reused_get_output_format_result_;
+
+  SB_DISALLOW_COPY_AND_ASSIGN(MediaCodecBridge);
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_MEDIA_CODEC_BRIDGE_H_
diff --git a/src/starboard/android/shared/media_common.h b/src/starboard/android/shared/media_common.h
new file mode 100644
index 0000000..78ba783
--- /dev/null
+++ b/src/starboard/android/shared/media_common.h
@@ -0,0 +1,102 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
+
+#include <deque>
+#include <queue>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/media.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/player/filter/audio_frame_tracker.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+const int64_t kSecondInMicroseconds = 1000 * 1000;
+
+inline bool IsWidevine(const char* key_system) {
+  return SbStringCompareAll(key_system, "com.widevine") == 0 ||
+         SbStringCompareAll(key_system, "com.widevine.alpha") == 0;
+}
+
+// Map a supported |SbMediaAudioCodec| into its corresponding mime type
+// string.  Returns |NULL| if |audio_codec| is not supported.
+inline const char* SupportedAudioCodecToMimeType(
+    const SbMediaAudioCodec audio_codec) {
+  if (audio_codec == kSbMediaAudioCodecAac) {
+    return "audio/mp4a-latm";
+  }
+  return NULL;
+}
+
+// Map a supported |SbMediaVideoCodec| into its corresponding mime type
+// string.  Returns |NULL| if |video_codec| is not supported.
+inline const char* SupportedVideoCodecToMimeType(
+    const SbMediaVideoCodec video_codec) {
+  if (video_codec == kSbMediaVideoCodecVp9) {
+    return "video/x-vnd.on2.vp9";
+  } else if (video_codec == kSbMediaVideoCodecH264) {
+    return "video/avc";
+  }
+  return NULL;
+}
+
+// A simple thread-safe queue for events of type |E|, that supports polling
+// based access only.
+template <typename E>
+class EventQueue {
+ public:
+  E PollFront() {
+    ScopedLock lock(mutex_);
+    if (!deque_.empty()) {
+      E event = deque_.front();
+      deque_.pop_front();
+      return event;
+    }
+
+    return E();
+  }
+
+  void PushBack(const E& event) {
+    ScopedLock lock(mutex_);
+    deque_.push_back(event);
+  }
+
+  void Clear() {
+    ScopedLock lock(mutex_);
+    deque_.clear();
+  }
+
+  size_t size() const {
+    ScopedLock lock(mutex_);
+    return deque_.size();
+  }
+
+ private:
+  ::starboard::Mutex mutex_;
+  std::deque<E> deque_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_MEDIA_COMMON_H_
diff --git a/src/starboard/android/shared/media_decoder.cc b/src/starboard/android/shared/media_decoder.cc
new file mode 100644
index 0000000..507239e
--- /dev/null
+++ b/src/starboard/android/shared/media_decoder.cc
@@ -0,0 +1,393 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/media_decoder.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/audio_sink.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+const jlong kDequeueTimeout = 0;
+
+const jint kNoOffset = 0;
+const jlong kNoPts = 0;
+const jint kNoSize = 0;
+const jint kNoBufferFlags = 0;
+
+const char* GetNameForMediaCodecStatus(jint status) {
+  switch (status) {
+    case MEDIA_CODEC_OK:
+      return "MEDIA_CODEC_OK";
+    case MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER:
+      return "MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER";
+    case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER:
+      return "MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER";
+    case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED:
+      return "MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED";
+    case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED:
+      return "MEDIA_CODEC_OUTPUT_FORMAT_CHANGED";
+    case MEDIA_CODEC_INPUT_END_OF_STREAM:
+      return "MEDIA_CODEC_INPUT_END_OF_STREAM";
+    case MEDIA_CODEC_OUTPUT_END_OF_STREAM:
+      return "MEDIA_CODEC_OUTPUT_END_OF_STREAM";
+    case MEDIA_CODEC_NO_KEY:
+      return "MEDIA_CODEC_NO_KEY";
+    case MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION:
+      return "MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION";
+    case MEDIA_CODEC_ABORT:
+      return "MEDIA_CODEC_ABORT";
+    case MEDIA_CODEC_ERROR:
+      return "MEDIA_CODEC_ERROR";
+    default:
+      SB_NOTREACHED();
+      return "MEDIA_CODEC_ERROR_UNKNOWN";
+  }
+}
+
+}  // namespace
+
+MediaDecoder::MediaDecoder(Host* host,
+                           SbMediaAudioCodec audio_codec,
+                           const SbMediaAudioHeader& audio_header,
+                           SbDrmSystem drm_system)
+    : media_type_(kSbMediaTypeAudio),
+      host_(host),
+      decoder_thread_(kSbThreadInvalid),
+      media_codec_bridge_(NULL),
+      stream_ended_(false),
+      drm_system_(static_cast<DrmSystem*>(drm_system)) {
+  SB_DCHECK(host_);
+
+  jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+  SB_DCHECK(!drm_system_ || j_media_crypto);
+  media_codec_bridge_ = MediaCodecBridge::CreateAudioMediaCodecBridge(
+      audio_codec, audio_header, j_media_crypto);
+  if (!media_codec_bridge_) {
+    SB_LOG(ERROR) << "Failed to create audio media codec bridge.";
+    return;
+  }
+  if (audio_header.audio_specific_config_size > 0) {
+    // |audio_header.audio_specific_config| is guaranteed to be outlived the
+    // decoder as it is stored in |FilterBasedPlayerWorkerHandler|.
+    event_queue_.PushBack(Event(
+        static_cast<const int8_t*>(audio_header.audio_specific_config),
+        audio_header.audio_specific_config_size));
+  }
+}
+
+MediaDecoder::MediaDecoder(Host* host,
+                           SbMediaVideoCodec video_codec,
+                           int width,
+                           int height,
+                           jobject j_output_surface,
+                           SbDrmSystem drm_system,
+                           const SbMediaColorMetadata* color_metadata)
+    : media_type_(kSbMediaTypeVideo),
+      host_(host),
+      stream_ended_(false),
+      drm_system_(static_cast<DrmSystem*>(drm_system)),
+      decoder_thread_(kSbThreadInvalid),
+      media_codec_bridge_(NULL) {
+  jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+  SB_DCHECK(!drm_system_ || j_media_crypto);
+  media_codec_bridge_ = MediaCodecBridge::CreateVideoMediaCodecBridge(
+      video_codec, width, height, j_output_surface, j_media_crypto,
+      color_metadata);
+  if (!media_codec_bridge_) {
+    SB_LOG(ERROR) << "Failed to create video media codec bridge.";
+  }
+}
+
+MediaDecoder::~MediaDecoder() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  JoinOnDecoderThread();
+}
+
+void MediaDecoder::Initialize(const ErrorCB& error_cb) {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+  SB_DCHECK(error_cb);
+  SB_DCHECK(!error_cb_);
+
+  error_cb_ = error_cb;
+}
+
+void MediaDecoder::WriteInputBuffer(
+    const scoped_refptr<InputBuffer>& input_buffer) {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+  SB_DCHECK(input_buffer);
+
+  if (stream_ended_) {
+    SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
+    return;
+  }
+
+  if (!SbThreadIsValid(decoder_thread_)) {
+    decoder_thread_ = SbThreadCreate(
+        0, kSbThreadPriorityNormal, kSbThreadNoAffinity, true,
+        media_type_ == kSbMediaTypeAudio ? "audio_decoder" : "video_decoder",
+        &MediaDecoder::ThreadEntryPoint, this);
+    SB_DCHECK(SbThreadIsValid(decoder_thread_));
+  }
+
+  event_queue_.PushBack(Event(input_buffer));
+}
+
+void MediaDecoder::WriteEndOfStream() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  stream_ended_ = true;
+  event_queue_.PushBack(Event(Event::kWriteEndOfStream));
+}
+
+// static
+void* MediaDecoder::ThreadEntryPoint(void* context) {
+  SB_DCHECK(context);
+  MediaDecoder* decoder = static_cast<MediaDecoder*>(context);
+  decoder->DecoderThreadFunc();
+  return NULL;
+}
+
+void MediaDecoder::DecoderThreadFunc() {
+  SB_DCHECK(error_cb_);
+
+  // TODO: Replace |pending_work| with a single object instead of using a deque.
+  std::deque<Event> pending_work;
+
+  // TODO: Refactor the i/o logic using async based decoder.
+  while (!destroying_.load()) {
+    if (pending_work.empty()) {
+      Event event = event_queue_.PollFront();
+
+      if (event.type == Event::kWriteInputBuffer ||
+          event.type == Event::kWriteEndOfStream ||
+          event.type == Event::kWriteCodecConfig) {
+        pending_work.push_back(event);
+      }
+    }
+
+    if (media_type_ == kSbMediaTypeAudio) {
+      if (!ProcessOneInputBuffer(&pending_work) &&
+          !DequeueAndProcessOutputBuffer()) {
+        SbThreadSleep(kSbTimeMillisecond);
+      }
+      continue;
+    }
+
+    SB_DCHECK(media_type_ == kSbMediaTypeVideo);
+    // Call Tick() to give the video decoder a chance to release the frames
+    // after each input or output operations.
+    if (!ProcessOneInputBuffer(&pending_work) &&
+        !host_->Tick(media_codec_bridge_.get())) {
+      SbThreadSleep(kSbTimeMillisecond);
+    }
+    if (!DequeueAndProcessOutputBuffer() &&
+        !host_->Tick(media_codec_bridge_.get())) {
+      SbThreadSleep(kSbTimeMillisecond);
+    }
+  }
+
+  SB_LOG(INFO) << "Destroying decoder thread.";
+  host_->OnFlushing();
+  jint status = media_codec_bridge_->Flush();
+  if (status != MEDIA_CODEC_OK) {
+    SB_LOG(ERROR) << "Failed to flush media codec.";
+  }
+}
+
+void MediaDecoder::JoinOnDecoderThread() {
+  if (!SbThreadIsValid(decoder_thread_)) {
+    return;
+  }
+  destroying_.store(true);
+  SbThreadJoin(decoder_thread_, NULL);
+  event_queue_.Clear();
+  decoder_thread_ = kSbThreadInvalid;
+}
+
+bool MediaDecoder::ProcessOneInputBuffer(std::deque<Event>* pending_work) {
+  SB_DCHECK(pending_work);
+  if (pending_work->empty()) {
+    return false;
+  }
+
+  SB_CHECK(media_codec_bridge_);
+
+  // During secure playback, and only secure playback, is is possible that our
+  // attempt to enqueue an input buffer will be rejected by MediaCodec because
+  // we do not have a key yet.  In this case, we hold on to the input buffer
+  // that we have already set up, and repeatedly attempt to enqueue it until
+  // it works.  Ideally, we would just wait until MediaDrm was ready, however
+  // the shared starboard player framework assumes that it is possible to
+  // perform decryption and decoding as separate steps, so from its
+  // perspective, having made it to this point implies that we ready to
+  // decode.  It is not possible to do them as separate steps on Android. From
+  // the perspective of user application, decryption and decoding are one
+  // atomic step.
+  DequeueInputResult dequeue_input_result;
+  Event event;
+  bool input_buffer_already_written = false;
+  if (pending_queue_input_buffer_task_) {
+    dequeue_input_result =
+        pending_queue_input_buffer_task_->dequeue_input_result;
+    SB_DCHECK(dequeue_input_result.index >= 0);
+    event = pending_queue_input_buffer_task_->event;
+    pending_queue_input_buffer_task_ = nullopt_t();
+    input_buffer_already_written = true;
+  } else {
+    dequeue_input_result =
+        media_codec_bridge_->DequeueInputBuffer(kDequeueTimeout);
+    event = pending_work->front();
+    if (dequeue_input_result.index < 0) {
+      HandleError("dequeueInputBuffer", dequeue_input_result.status);
+      return false;
+    }
+    pending_work->pop_front();
+  }
+
+  SB_DCHECK(event.type == Event::kWriteCodecConfig ||
+            event.type == Event::kWriteInputBuffer ||
+            event.type == Event::kWriteEndOfStream);
+  const scoped_refptr<InputBuffer>& input_buffer = event.input_buffer;
+  if (event.type == Event::kWriteEndOfStream) {
+    SB_DCHECK(pending_work->empty());
+  }
+  const void* data = NULL;
+  int size = 0;
+  if (event.type == Event::kWriteCodecConfig) {
+    SB_DCHECK(media_type_ == kSbMediaTypeAudio);
+    data = event.codec_config;
+    size = event.codec_config_size;
+  } else if (event.type == Event::kWriteInputBuffer) {
+    data = input_buffer->data();
+    size = input_buffer->size();
+  } else if (event.type == Event::kWriteEndOfStream) {
+    data = NULL;
+    size = 0;
+  }
+
+  // Don't bother rewriting the same data if we already did it last time we
+  // were called and had it stored in |pending_queue_input_buffer_task_|.
+  if (!input_buffer_already_written && event.type != Event::kWriteEndOfStream) {
+    ScopedJavaByteBuffer byte_buffer(
+        media_codec_bridge_->GetInputBuffer(dequeue_input_result.index));
+    if (byte_buffer.IsNull() || byte_buffer.capacity() < size) {
+      SB_LOG(ERROR) << "Unable to write to MediaCodec input buffer.";
+      return false;
+    }
+    byte_buffer.CopyInto(data, size);
+  }
+
+  jint status;
+  if (event.type == Event::kWriteCodecConfig) {
+    status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index,
+                                                   kNoOffset, size, kNoPts,
+                                                   BUFFER_FLAG_CODEC_CONFIG);
+  } else if (event.type == Event::kWriteInputBuffer) {
+    jlong pts_us = input_buffer->timestamp();
+    if (drm_system_ && input_buffer->drm_info()) {
+      status = media_codec_bridge_->QueueSecureInputBuffer(
+          dequeue_input_result.index, kNoOffset, *input_buffer->drm_info(),
+          pts_us);
+    } else {
+      status = media_codec_bridge_->QueueInputBuffer(
+          dequeue_input_result.index, kNoOffset, size, pts_us, kNoBufferFlags);
+    }
+  } else {
+    status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index,
+                                                   kNoOffset, size, kNoPts,
+                                                   BUFFER_FLAG_END_OF_STREAM);
+  }
+
+  if (status != MEDIA_CODEC_OK) {
+    HandleError("queue(Secure)?InputBuffer", status);
+    // TODO: Stop the decoding loop on fatal error.
+    SB_DCHECK(!pending_queue_input_buffer_task_);
+    pending_queue_input_buffer_task_ = {dequeue_input_result, event};
+    return false;
+  }
+
+  return true;
+}
+
+bool MediaDecoder::DequeueAndProcessOutputBuffer() {
+  SB_CHECK(media_codec_bridge_);
+
+  DequeueOutputResult dequeue_output_result =
+      media_codec_bridge_->DequeueOutputBuffer(kDequeueTimeout);
+
+  // Note that if the |index| field of |DequeueOutputResult| is negative, then
+  // all fields other than |status| and |index| are invalid.  This is
+  // especially important, as the Java side of |MediaCodecBridge| will reuse
+  // objects for returned results behind the scenes.
+  if (dequeue_output_result.index < 0) {
+    if (dequeue_output_result.status == MEDIA_CODEC_OUTPUT_FORMAT_CHANGED) {
+      host_->RefreshOutputFormat(media_codec_bridge_.get());
+      return true;
+    }
+
+    if (dequeue_output_result.status == MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED) {
+      SB_DLOG(INFO) << "Output buffers changed, trying to dequeue again.";
+      return true;
+    }
+
+    HandleError("dequeueOutputBuffer", dequeue_output_result.status);
+    return false;
+  }
+
+  host_->ProcessOutputBuffer(media_codec_bridge_.get(), dequeue_output_result);
+  return true;
+}
+
+void MediaDecoder::HandleError(const char* action_name, jint status) {
+  SB_DCHECK(status != MEDIA_CODEC_OK);
+
+  bool retry = false;
+  if (status == MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER) {
+    // Don't bother logging a try again later status, it happens a lot.
+    return;
+  } else if (status == MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER) {
+    // Don't bother logging a try again later status, it will happen a lot.
+    return;
+  } else if (status == MEDIA_CODEC_NO_KEY) {
+    retry = true;
+  } else if (status == MEDIA_CODEC_INSUFFICIENT_OUTPUT_PROTECTION) {
+    drm_system_->OnInsufficientOutputProtection();
+  } else {
+    error_cb_(kSbPlayerErrorDecode,
+              FormatString("%s failed with status %d.", action_name, status));
+  }
+
+  if (retry) {
+    SB_LOG(INFO) << "|" << action_name << "| failed with status: "
+                 << GetNameForMediaCodecStatus(status)
+                 << ", will try again after a delay.";
+  } else {
+    SB_LOG(ERROR) << "|" << action_name << "| failed with status: "
+                  << GetNameForMediaCodecStatus(status) << ".";
+  }
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/media_decoder.h b/src/starboard/android/shared/media_decoder.h
new file mode 100644
index 0000000..226c71d
--- /dev/null
+++ b/src/starboard/android/shared/media_decoder.h
@@ -0,0 +1,160 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
+
+#include <jni.h>
+
+#include <deque>
+#include <functional>
+#include <queue>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/atomic.h"
+#include "starboard/common/optional.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/filter/callback.h"
+#include "starboard/shared/starboard/player/input_buffer_internal.h"
+#include "starboard/shared/starboard/thread_checker.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// TODO: Better encapsulation the MediaCodecBridge so the decoders no longer
+//       need to talk directly to the MediaCodecBridge.
+class MediaDecoder {
+ public:
+  typedef ::starboard::shared::starboard::player::filter::ErrorCB ErrorCB;
+  typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+
+  // This class should be implemented by the users of MediaDecoder to receive
+  // various notifications.  Note that all such functions are called on the
+  // decoder thread.
+  class Host {
+   public:
+    virtual void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+                                     const DequeueOutputResult& output) = 0;
+    virtual void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) = 0;
+    // This function gets called frequently on the decoding thread to give the
+    // Host a chance to process when the MediaDecoder is decoding video.
+    // TODO: Revise the scheduling logic to give the host a chance to process in
+    //       a more elegant way.
+    virtual bool Tick(MediaCodecBridge* media_codec_bridge) = 0;
+    // This function gets called before calling Flush() on the contained
+    // MediaCodecBridge so the host can have a chance to do necessary cleanups
+    // before the MediaCodecBridge is flushed.
+    virtual void OnFlushing() = 0;
+
+   protected:
+    ~Host() {}
+  };
+
+  MediaDecoder(Host* host,
+               SbMediaAudioCodec audio_codec,
+               const SbMediaAudioHeader& audio_header,
+               SbDrmSystem drm_system);
+  MediaDecoder(Host* host,
+               SbMediaVideoCodec video_codec,
+               int width,
+               int height,
+               jobject j_output_surface,
+               SbDrmSystem drm_system,
+               const SbMediaColorMetadata* color_metadata);
+  ~MediaDecoder();
+
+  void Initialize(const ErrorCB& error_cb);
+  void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+  void WriteEndOfStream();
+
+  size_t GetNumberOfPendingTasks() const { return event_queue_.size(); }
+
+  bool is_valid() const { return media_codec_bridge_ != NULL; }
+
+ private:
+  struct Event {
+    enum Type {
+      kInvalid,
+      kWriteCodecConfig,
+      kWriteInputBuffer,
+      kWriteEndOfStream,
+    };
+
+    explicit Event(Type type = kInvalid) : type(type) {
+      SB_DCHECK(type != kWriteInputBuffer && type != kWriteCodecConfig);
+    }
+    Event(const int8_t* codec_config, int16_t codec_config_size)
+        : type(kWriteCodecConfig),
+          codec_config(codec_config),
+          codec_config_size(codec_config_size) {}
+    explicit Event(const scoped_refptr<InputBuffer>& input_buffer)
+        : type(kWriteInputBuffer), input_buffer(input_buffer) {}
+
+    Type type;
+    scoped_refptr<InputBuffer> input_buffer;
+    const int8_t* codec_config;
+    int16_t codec_config_size;
+  };
+
+  struct QueueInputBufferTask {
+    DequeueInputResult dequeue_input_result;
+    Event event;
+  };
+
+  static void* ThreadEntryPoint(void* context);
+  void DecoderThreadFunc();
+  void JoinOnDecoderThread();
+
+  void TeardownCodec();
+
+  bool ProcessOneInputBuffer(std::deque<Event>* pending_work);
+  // Attempt to dequeue a media codec output buffer.  Returns whether the
+  // processing should continue.  If a valid buffer is dequeued, it will call
+  // ProcessOutputBuffer() on host internally.  It is the responsibility of
+  // ProcessOutputBuffer() to release the output buffer back to the system.
+  bool DequeueAndProcessOutputBuffer();
+
+  void HandleError(const char* action_name, jint status);
+
+  ::starboard::shared::starboard::ThreadChecker thread_checker_;
+
+  const SbMediaType media_type_;
+  Host* host_;
+  ErrorCB error_cb_;
+
+  // Working thread to avoid lengthy decoding work block the player thread.
+  SbThread decoder_thread_;
+  scoped_ptr<MediaCodecBridge> media_codec_bridge_;
+
+  bool stream_ended_;
+
+  DrmSystem* drm_system_;
+
+  // Events are processed in a queue, except for when handling events of type
+  // |kReset|, which are allowed to cut to the front.
+  EventQueue<Event> event_queue_;
+  atomic_bool destroying_;
+
+  optional<QueueInputBufferTask> pending_queue_input_buffer_task_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_MEDIA_DECODER_H_
diff --git a/src/starboard/android/shared/media_get_audio_configuration.cc b/src/starboard/android/shared/media_get_audio_configuration.cc
new file mode 100644
index 0000000..ef59d08
--- /dev/null
+++ b/src/starboard/android/shared/media_get_audio_configuration.cc
@@ -0,0 +1,50 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbMediaGetAudioConfiguration(
+    int output_index,
+    SbMediaAudioConfiguration* out_configuration) {
+  if (output_index != 0 || out_configuration == NULL) {
+    return false;
+  }
+
+  out_configuration->index = 0;
+  out_configuration->connector = kSbMediaAudioConnectorHdmi;
+  out_configuration->latency = 0;
+  out_configuration->coding_type = kSbMediaAudioCodingTypePcm;
+
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_audio_output_manager(
+      env->CallStarboardObjectMethodOrAbort(
+          "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;"));
+  int channels = static_cast<int>(env->CallIntMethodOrAbort(
+      j_audio_output_manager.Get(), "getMaxChannels", "()I"));
+  if (channels < 2) {
+    SB_DLOG(WARNING)
+        << "The supported channels from output device is smaller than 2. "
+           "Fallback to 2 channels";
+    out_configuration->number_of_channels = 2;
+  } else {
+    out_configuration->number_of_channels = channels;
+  }
+  return true;
+}
diff --git a/src/starboard/android/shared/media_get_audio_output_count.cc b/src/starboard/android/shared/media_get_audio_output_count.cc
new file mode 100644
index 0000000..befefd3
--- /dev/null
+++ b/src/starboard/android/shared/media_get_audio_output_count.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetAudioOutputCount() {
+  // Only supports one HDMI output.
+  return 1;
+}
diff --git a/src/starboard/android/shared/media_get_initial_buffer_capacity.cc b/src/starboard/android/shared/media_get_initial_buffer_capacity.cc
new file mode 100644
index 0000000..1634373
--- /dev/null
+++ b/src/starboard/android/shared/media_get_initial_buffer_capacity.cc
@@ -0,0 +1,19 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetInitialBufferCapacity() {
+  return 80 * 1024 * 1024;
+}
diff --git a/src/starboard/android/shared/media_get_max_buffer_capacity.cc b/src/starboard/android/shared/media_get_max_buffer_capacity.cc
new file mode 100644
index 0000000..1d3899b
--- /dev/null
+++ b/src/starboard/android/shared/media_get_max_buffer_capacity.cc
@@ -0,0 +1,29 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+int SbMediaGetMaxBufferCapacity(SbMediaVideoCodec codec,
+                                int resolution_width,
+                                int resolution_height,
+                                int bits_per_pixel) {
+  SB_UNREFERENCED_PARAMETER(codec);
+  SB_UNREFERENCED_PARAMETER(resolution_width);
+  SB_UNREFERENCED_PARAMETER(resolution_height);
+  SB_UNREFERENCED_PARAMETER(bits_per_pixel);
+  // TODO: refine this to a more reasonable value, taking into account
+  // resolution. On most platforms this is 36 * 1024 * 1024 for 1080p, and
+  // 65 * 1024 * 1024 for 4k.
+  return 500 * 1024 * 1024;
+}
diff --git a/src/starboard/android/shared/media_is_audio_supported.cc b/src/starboard/android/shared/media_is_audio_supported.cc
new file mode 100644
index 0000000..8994965
--- /dev/null
+++ b/src/starboard/android/shared/media_is_audio_supported.cc
@@ -0,0 +1,38 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/media/media_support_internal.h"
+
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/configuration.h"
+#include "starboard/media.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+using starboard::android::shared::SupportedAudioCodecToMimeType;
+
+SB_EXPORT bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec,
+                                       int64_t bitrate) {
+  const char* mime = SupportedAudioCodecToMimeType(audio_codec);
+  if (!mime) {
+    return false;
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+  return env->CallStaticBooleanMethodOrAbort(
+             "dev/cobalt/media/MediaCodecUtil", "hasAudioDecoderFor",
+             "(Ljava/lang/String;I)Z", j_mime.Get(),
+             static_cast<jint>(bitrate)) == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_is_output_protected.cc b/src/starboard/android/shared/media_is_output_protected.cc
new file mode 100644
index 0000000..141e75e
--- /dev/null
+++ b/src/starboard/android/shared/media_is_output_protected.cc
@@ -0,0 +1,26 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/log.h"
+
+bool SbMediaIsOutputProtected() {
+  // Protected output is expected to be properly enforced by the system level
+  // DRM system, and also does not provide a convenient way to query its
+  // state.  Thus, from a starboard application perspective, we always return
+  // |true|, and rely on playback of protected content failing at a later
+  // point if the output was not actually secure.
+  return true;
+}
diff --git a/src/starboard/android/shared/media_is_supported.cc b/src/starboard/android/shared/media_is_supported.cc
new file mode 100644
index 0000000..c7b150a
--- /dev/null
+++ b/src/starboard/android/shared/media_is_supported.cc
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/media_common.h"
+
+SB_EXPORT bool SbMediaIsSupported(SbMediaVideoCodec video_codec,
+                                  SbMediaAudioCodec audio_codec,
+                                  const char* key_system) {
+  using starboard::android::shared::IsWidevine;
+  using starboard::android::shared::JniEnvExt;
+  if (!IsWidevine(key_system)) {
+    return false;
+  }
+  return JniEnvExt::Get()->CallStaticBooleanMethodOrAbort(
+             "dev/cobalt/media/MediaDrmBridge",
+             "isWidevineCryptoSchemeSupported", "()Z") == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_is_video_supported.cc b/src/starboard/android/shared/media_is_video_supported.cc
new file mode 100644
index 0000000..ed29410
--- /dev/null
+++ b/src/starboard/android/shared/media_is_video_supported.cc
@@ -0,0 +1,93 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/media/media_support_internal.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/configuration.h"
+#include "starboard/media.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+using starboard::android::shared::SupportedVideoCodecToMimeType;
+
+namespace {
+
+// https://developer.android.com/reference/android/view/Display.HdrCapabilities.html#HDR_TYPE_HDR10
+const jint HDR_TYPE_DOLBY_VISION = 1;
+const jint HDR_TYPE_HDR10 = 2;
+const jint HDR_TYPE_HLG = 3;
+
+bool IsTransferCharacteristicsSupported(SbMediaTransferId transfer_id) {
+  // Bt709 and unspecified are assumed to always be supported.
+  if (transfer_id == kSbMediaTransferIdBt709 ||
+      transfer_id == kSbMediaTransferIdUnspecified) {
+    return true;
+  }
+  // An HDR capable VP9 decoder is needed to handle HDR at all.
+  bool has_hdr_capable_vp9_decoder =
+      JniEnvExt::Get()->CallStaticBooleanMethodOrAbort(
+          "dev/cobalt/media/MediaCodecUtil", "hasHdrCapableVp9Decoder",
+          "()Z") == JNI_TRUE;
+  if (!has_hdr_capable_vp9_decoder) {
+    return false;
+  }
+
+  jint hdr_type;
+  if (transfer_id == kSbMediaTransferIdSmpteSt2084) {
+    hdr_type = HDR_TYPE_HDR10;
+  } else if (transfer_id == kSbMediaTransferIdAribStdB67) {
+    hdr_type = HDR_TYPE_HLG;
+  } else {
+    // No other transfer functions are supported, see
+    // https://source.android.com/devices/tech/display/hdr.
+    return false;
+  }
+
+  return JniEnvExt::Get()->CallStarboardBooleanMethodOrAbort(
+             "isHdrTypeSupported", "(I)Z", hdr_type) == JNI_TRUE;
+}
+
+}  // namespace
+
+SB_EXPORT bool SbMediaIsVideoSupported(SbMediaVideoCodec video_codec,
+                                       int frame_width,
+                                       int frame_height,
+                                       int64_t bitrate,
+                                       int fps,
+                                       bool decode_to_texture_required,
+                                       SbMediaTransferId eotf) {
+  if (!IsTransferCharacteristicsSupported(eotf)) {
+    return false;
+  }
+  // While not necessarily true, for now we assume that all Android devices
+  // can play decode-to-texture video just as well as normal video.
+  SB_UNREFERENCED_PARAMETER(decode_to_texture_required);
+
+  const char* mime = SupportedVideoCodecToMimeType(video_codec);
+  if (!mime) {
+    return false;
+  }
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jstring> j_mime(env->NewStringStandardUTFOrAbort(mime));
+  bool must_support_hdr = (eotf != kSbMediaTransferIdBt709 &&
+                           eotf != kSbMediaTransferIdUnspecified);
+  return env->CallStaticBooleanMethodOrAbort(
+             "dev/cobalt/media/MediaCodecUtil", "hasVideoDecoderFor",
+             "(Ljava/lang/String;ZIIIIZ)Z", j_mime.Get(), false, frame_width,
+             frame_height, static_cast<jint>(bitrate), fps,
+             must_support_hdr) == JNI_TRUE;
+}
diff --git a/src/starboard/android/shared/media_set_output_protection.cc b/src/starboard/android/shared/media_set_output_protection.cc
new file mode 100644
index 0000000..a3de2d9
--- /dev/null
+++ b/src/starboard/android/shared/media_set_output_protection.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/media.h"
+
+#include "starboard/log.h"
+
+bool SbMediaSetOutputProtection(bool enabled) {
+  // Output is always protected from a starboard application perspective, so
+  // return a redundant true.  See documentation in |SbMediaIsOutputProtected|
+  // for further details.
+  return true;
+}
diff --git a/src/starboard/android/shared/microphone_impl.cc b/src/starboard/android/shared/microphone_impl.cc
new file mode 100644
index 0000000..ffff860
--- /dev/null
+++ b/src/starboard/android/shared/microphone_impl.cc
@@ -0,0 +1,540 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/microphone/microphone_internal.h"
+
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+
+#include <algorithm>
+#include <queue>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/shared/starboard/thread_checker.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace starboard {
+namespace android {
+namespace shared {
+namespace {
+
+const int kSampleRateInHz = 16000;
+const int kSampleRateInMillihertz = kSampleRateInHz * 1000;
+const int kNumOfOpenSLESBuffers = 2;
+const int kSamplesPerBuffer = 128;
+const int kBufferSizeInBytes = kSamplesPerBuffer * sizeof(int16_t);
+
+bool CheckReturnValue(SLresult result) {
+  return result == SL_RESULT_SUCCESS;
+}
+}  // namespace
+
+class SbMicrophoneImpl : public SbMicrophonePrivate {
+ public:
+  SbMicrophoneImpl();
+  ~SbMicrophoneImpl() SB_OVERRIDE;
+
+  bool Open() SB_OVERRIDE;
+  bool Close() SB_OVERRIDE;
+  int Read(void* out_audio_data, int audio_data_size) SB_OVERRIDE;
+
+  void SetPermission(bool is_granted);
+  static bool IsMicrophoneDisconnected();
+  static bool IsMicrophoneMute();
+
+ private:
+  enum State { kWaitPermission, kPermissionGranted, kOpened, kClosed };
+
+  static void SwapAndPublishBuffer(SLAndroidSimpleBufferQueueItf buffer_object,
+                                   void* context);
+  void SwapAndPublishBuffer();
+
+  bool CreateAudioRecorder();
+  void DeleteAudioRecorder();
+
+  void ClearBuffer();
+
+  bool RequestAudioPermission();
+  bool StartRecording();
+  bool StopRecording();
+
+  SLObjectItf engine_object_;
+  SLEngineItf engine_;
+  SLObjectItf recorder_object_;
+  SLRecordItf recorder_;
+  SLAndroidSimpleBufferQueueItf buffer_object_;
+  SLAndroidConfigurationItf config_object_;
+
+  // Keeps track of the microphone's current state.
+  State state_;
+  // Audio data that has been delivered to the buffer queue.
+  std::queue<int16_t*> delivered_queue_;
+  // Audio data that is ready to be read.
+  std::queue<int16_t*> ready_queue_;
+};
+
+SbMicrophoneImpl::SbMicrophoneImpl()
+    : engine_object_(nullptr),
+      engine_(nullptr),
+      recorder_object_(nullptr),
+      recorder_(nullptr),
+      buffer_object_(nullptr),
+      config_object_(nullptr),
+      state_(kClosed) {}
+
+SbMicrophoneImpl::~SbMicrophoneImpl() {
+  Close();
+}
+
+bool SbMicrophoneImpl::RequestAudioPermission() {
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject j_audio_permission_requester =
+      static_cast<jobject>(env->CallStarboardObjectMethodOrAbort(
+          "getAudioPermissionRequester",
+          "()Ldev/cobalt/coat/AudioPermissionRequester;"));
+  jboolean j_permission = env->CallBooleanMethodOrAbort(
+      j_audio_permission_requester, "requestRecordAudioPermission", "(J)Z",
+      reinterpret_cast<intptr_t>(this));
+  return j_permission;
+}
+
+// static
+bool SbMicrophoneImpl::IsMicrophoneDisconnected() {
+  JniEnvExt* env = JniEnvExt::Get();
+  jboolean j_microphone =
+      env->CallStarboardBooleanMethodOrAbort("isMicrophoneDisconnected", "()Z");
+  return j_microphone;
+}
+
+// static
+bool SbMicrophoneImpl::IsMicrophoneMute() {
+  JniEnvExt* env = JniEnvExt::Get();
+  jboolean j_microphone =
+      env->CallStarboardBooleanMethodOrAbort("isMicrophoneMute", "()Z");
+  return j_microphone;
+}
+
+bool SbMicrophoneImpl::Open() {
+  if (state_ == kOpened) {
+    // The microphone has already been opened; clear the unread buffer. See
+    // starboard/microphone.h for more info.
+    ClearBuffer();
+    return true;
+  }
+
+  if (IsMicrophoneDisconnected()) {
+    SB_DLOG(WARNING) << "No microphone connected.";
+    return false;
+  } else if (!RequestAudioPermission()) {
+    state_ = kWaitPermission;
+    SB_DLOG(INFO) << "Waiting for audio permission.";
+    // The permission is not set; this causes the MicrophoneManager to call
+    // read() repeatedly and wait for the user's response.
+    return true;
+  } else if (!StartRecording()) {
+    SB_DLOG(WARNING) << "Error starting recording.";
+    return false;
+  }
+
+  // Successfully opened the microphone and started recording.
+  state_ = kOpened;
+  return true;
+}
+
+bool SbMicrophoneImpl::StartRecording() {
+  if (!CreateAudioRecorder()) {
+    SB_DLOG(WARNING) << "Create audio recorder failed.";
+    DeleteAudioRecorder();
+    return false;
+  }
+
+  // Enqueues kNumOfOpenSLESBuffers zero buffers to start.
+  // Adds buffers to the queue before changing state to ensure that recording
+  // starts as soon as the state is modified.
+  for (int i = 0; i < kNumOfOpenSLESBuffers; ++i) {
+    int16_t* buffer = new int16_t[kSamplesPerBuffer];
+    SbMemorySet(buffer, 0, kBufferSizeInBytes);
+    delivered_queue_.push(buffer);
+    SLresult result =
+        (*buffer_object_)->Enqueue(buffer_object_, buffer, kBufferSizeInBytes);
+    if (!CheckReturnValue(result)) {
+      SB_DLOG(WARNING) << "Error adding buffers to the queue.";
+      return false;
+    }
+  }
+
+  // Start the recording by setting the state to |SL_RECORDSTATE_RECORDING|.
+  // When the object is in the SL_RECORDSTATE_RECORDING state, adding buffers
+  // will implicitly start the recording process.
+  SLresult result =
+      (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_RECORDING);
+  if (!CheckReturnValue(result)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool SbMicrophoneImpl::Close() {
+  if (state_ == kClosed) {
+    // The microphone has already been closed.
+    return true;
+  }
+
+  if (state_ == kOpened && !StopRecording()) {
+    SB_DLOG(WARNING) << "Error closing the microphone.";
+    return false;
+  }
+
+  // Successfully closed the microphone and stopped recording.
+  state_ = kClosed;
+  return true;
+}
+
+bool SbMicrophoneImpl::StopRecording() {
+  // Stop recording by setting the record state to |SL_RECORDSTATE_STOPPED|.
+  SLresult result =
+      (*recorder_)->SetRecordState(recorder_, SL_RECORDSTATE_STOPPED);
+  if (!CheckReturnValue(result)) {
+    return false;
+  }
+
+  ClearBuffer();
+
+  DeleteAudioRecorder();
+
+  return true;
+}
+
+int SbMicrophoneImpl::Read(void* out_audio_data, int audio_data_size) {
+  if (state_ == kClosed || IsMicrophoneMute()) {
+    // No audio data is read from a stopped or muted microphone; return an
+    // error.
+    return -1;
+  }
+
+  if (!out_audio_data || audio_data_size == 0 || state_ == kWaitPermission) {
+    // No data to be read.
+    return 0;
+  }
+
+  if (state_ == kPermissionGranted) {
+    if (StartRecording()) {
+      state_ = kOpened;
+    } else {
+      // Could not start recording; return an error.
+      state_ = kClosed;
+      return -1;
+    }
+  }
+
+  int read_bytes = 0;
+  scoped_ptr<int16_t> buffer;
+  // Go through the ready queue, reading and sending audio data.
+  while (!ready_queue_.empty() &&
+         audio_data_size - read_bytes >= kBufferSizeInBytes) {
+    buffer.reset(ready_queue_.front());
+    SbMemoryCopy(static_cast<uint8_t*>(out_audio_data) + read_bytes,
+                 buffer.get(), kBufferSizeInBytes);
+    ready_queue_.pop();
+    read_bytes += kBufferSizeInBytes;
+  }
+
+  buffer.reset();
+  return read_bytes;
+}
+
+void SbMicrophoneImpl::SetPermission(bool is_granted) {
+  state_ = is_granted ? kPermissionGranted : kClosed;
+}
+
+// static
+void SbMicrophoneImpl::SwapAndPublishBuffer(
+    SLAndroidSimpleBufferQueueItf buffer_object,
+    void* context) {
+  SbMicrophoneImpl* recorder = static_cast<SbMicrophoneImpl*>(context);
+  recorder->SwapAndPublishBuffer();
+}
+
+void SbMicrophoneImpl::SwapAndPublishBuffer() {
+  if (!delivered_queue_.empty()) {
+    // The front item in the delivered queue already has the buffered data, so
+    // move it from the delivered queue to the ready queue for future reads.
+    int16_t* buffer = delivered_queue_.front();
+    delivered_queue_.pop();
+    ready_queue_.push(buffer);
+  }
+
+  if (state_ == kOpened) {
+    int16_t* buffer = new int16_t[kSamplesPerBuffer];
+    SbMemorySet(buffer, 0, kBufferSizeInBytes);
+    delivered_queue_.push(buffer);
+    SLresult result =
+        (*buffer_object_)->Enqueue(buffer_object_, buffer, kBufferSizeInBytes);
+    CheckReturnValue(result);
+  }
+}
+
+bool SbMicrophoneImpl::CreateAudioRecorder() {
+  SLresult result;
+  // Initializes the SL engine object with specific options.
+  // OpenSL ES for Android is designed for multi-threaded applications and
+  // is thread-safe.
+  result = slCreateEngine(&engine_object_, 0, nullptr, 0, nullptr, nullptr);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error creating the SL engine object.";
+    return false;
+  }
+
+  // Realize the SL engine object in synchronous mode.
+  result = (*engine_object_)
+               ->Realize(engine_object_, /* async = */ SL_BOOLEAN_FALSE);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error realizing the SL engine object.";
+    return false;
+  }
+
+  // Get the SL engine interface.
+  result =
+      (*engine_object_)->GetInterface(engine_object_, SL_IID_ENGINE, &engine_);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error getting the SL engine interface.";
+    return false;
+  }
+
+  // Audio source configuration; the audio source is an I/O device data locator.
+  SLDataLocator_IODevice input_dev_locator = {
+      SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
+      SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr};
+
+  SLDataSource audio_source = {&input_dev_locator, nullptr};
+  SLDataLocator_AndroidSimpleBufferQueue simple_buffer_queue = {
+      SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, kNumOfOpenSLESBuffers};
+
+  // Audio sink configuration; the audio sink is a simple buffer queue. PCM is
+  // the only data format allowed with buffer queues.
+  SLAndroidDataFormat_PCM_EX format = {
+      SL_ANDROID_DATAFORMAT_PCM_EX, 1 /* numChannels */,
+      kSampleRateInMillihertz,      SL_PCMSAMPLEFORMAT_FIXED_16,
+      SL_PCMSAMPLEFORMAT_FIXED_16,  SL_SPEAKER_FRONT_CENTER,
+      SL_BYTEORDER_LITTLEENDIAN,    SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT};
+  SLDataSink audio_sink = {&simple_buffer_queue, &format};
+
+  const int kCount = 2;
+  const SLInterfaceID kInterfaceId[kCount] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+                                              SL_IID_ANDROIDCONFIGURATION};
+  const SLboolean kInterfaceRequired[kCount] = {SL_BOOLEAN_TRUE,
+                                                SL_BOOLEAN_TRUE};
+  // Create the audio recorder.
+  result = (*engine_)->CreateAudioRecorder(engine_, &recorder_object_,
+                                           &audio_source, &audio_sink, kCount,
+                                           kInterfaceId, kInterfaceRequired);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error creating the audio recorder.";
+    return false;
+  }
+
+  // Configure the audio recorder (before it is realized); get the configuration
+  // interface.
+  result = (*recorder_object_)
+               ->GetInterface(recorder_object_, SL_IID_ANDROIDCONFIGURATION,
+                              &config_object_);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error getting the audio recorder interface.";
+    return false;
+  }
+
+  // Use the main microphone tuned for voice recognition.
+  const SLuint32 kPresetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION;
+  result =
+      (*config_object_)
+          ->SetConfiguration(config_object_, SL_ANDROID_KEY_RECORDING_PRESET,
+                             &kPresetValue, sizeof(SLuint32));
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error configuring the audio recorder.";
+    return false;
+  }
+
+  // Realize the recorder in synchronous mode.
+  result = (*recorder_object_)
+               ->Realize(recorder_object_, /* async = */ SL_BOOLEAN_FALSE);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error realizing the audio recorder. Double check that "
+                        "the microphone is connected to the device.";
+    return false;
+  }
+
+  // Get the record interface (an implicit interface).
+  result = (*recorder_object_)
+               ->GetInterface(recorder_object_, SL_IID_RECORD, &recorder_);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error getting the audio recorder interface.";
+    return false;
+  }
+
+  // Get the buffer queue interface which was explicitly requested.
+  result = (*recorder_object_)
+               ->GetInterface(recorder_object_, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
+                              &buffer_object_);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error getting the buffer queue interface.";
+    return false;
+  }
+
+  // Setup to receive buffer queue event callbacks for when a buffer in the
+  // queue is completed.
+  result =
+      (*buffer_object_)
+          ->RegisterCallback(buffer_object_,
+                             &SbMicrophoneImpl::SwapAndPublishBuffer, this);
+  if (!CheckReturnValue(result)) {
+    SB_DLOG(WARNING) << "Error registering buffer queue callbacks.";
+    return false;
+  }
+
+  return true;
+}
+
+void SbMicrophoneImpl::DeleteAudioRecorder() {
+  if (recorder_object_) {
+    (*recorder_object_)->Destroy(recorder_object_);
+  }
+
+  config_object_ = nullptr;
+  buffer_object_ = nullptr;
+  recorder_ = nullptr;
+  recorder_object_ = nullptr;
+
+  // Destroy the engine object.
+  if (engine_object_) {
+    (*engine_object_)->Destroy(engine_object_);
+  }
+  engine_ = nullptr;
+  engine_object_ = nullptr;
+}
+
+void SbMicrophoneImpl::ClearBuffer() {
+  // Clear the buffer queue to get rid of old data.
+  if (buffer_object_) {
+    SLresult result = (*buffer_object_)->Clear(buffer_object_);
+    if (!CheckReturnValue(result)) {
+      SB_DLOG(WARNING) << "Error clearing the buffer.";
+    }
+  }
+
+  while (!delivered_queue_.empty()) {
+    delete[] delivered_queue_.front();
+    delivered_queue_.pop();
+  }
+
+  while (!ready_queue_.empty()) {
+    delete[] ready_queue_.front();
+    ready_queue_.pop();
+  }
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+int SbMicrophonePrivate::GetAvailableMicrophones(
+    SbMicrophoneInfo* out_info_array,
+    int info_array_size) {
+  // Note that there is no way of checking for a connected microphone/device
+  // before API 23, so GetAvailableMicrophones() will assume a microphone is
+  // connected and always return 1 on APIs < 23.
+  if (starboard::android::shared::SbMicrophoneImpl::
+          IsMicrophoneDisconnected()) {
+    SB_DLOG(WARNING) << "No microphone connected.";
+    return 0;
+  }
+  if (starboard::android::shared::SbMicrophoneImpl::IsMicrophoneMute()) {
+    SB_DLOG(WARNING) << "Microphone is muted.";
+    return 0;
+  }
+
+  if (out_info_array && info_array_size > 0) {
+    // Only support one microphone.
+    out_info_array[0].id = reinterpret_cast<SbMicrophoneId>(1);
+    out_info_array[0].type = kSbMicrophoneUnknown;
+    out_info_array[0].max_sample_rate_hz =
+        starboard::android::shared::kSampleRateInHz;
+    out_info_array[0].min_read_size =
+        starboard::android::shared::kSamplesPerBuffer;
+  }
+
+  return 1;
+}
+
+bool SbMicrophonePrivate::IsMicrophoneSampleRateSupported(
+    SbMicrophoneId id,
+    int sample_rate_in_hz) {
+  if (!SbMicrophoneIdIsValid(id)) {
+    return false;
+  }
+
+  return sample_rate_in_hz == starboard::android::shared::kSampleRateInHz;
+}
+
+namespace {
+const int kUnusedBufferSize = 32 * 1024;
+// Only a single microphone is supported.
+SbMicrophone s_microphone = kSbMicrophoneInvalid;
+
+}  // namespace
+
+SbMicrophone SbMicrophonePrivate::CreateMicrophone(SbMicrophoneId id,
+                                                   int sample_rate_in_hz,
+                                                   int buffer_size_bytes) {
+  if (!SbMicrophoneIdIsValid(id) ||
+      !IsMicrophoneSampleRateSupported(id, sample_rate_in_hz) ||
+      buffer_size_bytes > kUnusedBufferSize || buffer_size_bytes <= 0) {
+    return kSbMicrophoneInvalid;
+  }
+
+  if (s_microphone != kSbMicrophoneInvalid) {
+    return kSbMicrophoneInvalid;
+  }
+
+  s_microphone = new starboard::android::shared::SbMicrophoneImpl();
+  return s_microphone;
+}
+
+void SbMicrophonePrivate::DestroyMicrophone(SbMicrophone microphone) {
+  if (!SbMicrophoneIsValid(microphone)) {
+    return;
+  }
+
+  SB_DCHECK(s_microphone == microphone);
+  s_microphone->Close();
+
+  delete s_microphone;
+  s_microphone = kSbMicrophoneInvalid;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_AudioPermissionRequester_nativeHandlePermission(
+    JNIEnv* env,
+    jobject unused_this,
+    jlong nativeSbMicrophoneImpl,
+    jboolean is_granted) {
+  starboard::android::shared::SbMicrophoneImpl* native =
+      reinterpret_cast<starboard::android::shared::SbMicrophoneImpl*>(
+          nativeSbMicrophoneImpl);
+  native->SetPermission(is_granted);
+}
diff --git a/src/starboard/android/shared/platform_deploy.gypi b/src/starboard/android/shared/platform_deploy.gypi
new file mode 100644
index 0000000..3d6e60f
--- /dev/null
+++ b/src/starboard/android/shared/platform_deploy.gypi
@@ -0,0 +1,68 @@
+# Copyright 2017 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'variables': {
+    'GRADLE_BUILD_TYPE': '<(cobalt_config)',
+    'GRADLE_FILES_DIR': '<(PRODUCT_DIR)/gradle/<(executable_name)',
+    'executable_file': '<(PRODUCT_DIR)/lib/lib<(executable_name).so',
+    'apk': '<(PRODUCT_DIR)/<(executable_name).apk',
+  },
+  'conditions': [
+    [ 'cobalt_config == "gold"', {
+      'variables': {
+        # Android Gradle wants a "release" build type, so use that for "gold".
+        'GRADLE_BUILD_TYPE': 'release'
+      }
+    }]
+  ],
+  'dependencies': [ '<(DEPTH)/starboard/android/apk/apk.gyp:apk_sources' ],
+  'includes': [ '<(DEPTH)/starboard/build/collect_deploy_content.gypi' ],
+  'actions': [
+    {
+      'action_name': 'build_apk',
+      'inputs': [
+        '<(executable_file)',
+        '<(PRODUCT_DIR)/gradle/apk_sources.stamp',
+        '<(content_deploy_stamp_file)',
+      ],
+      'outputs': [ '<(apk)' ],
+      'action': [
+        '<(DEPTH)/starboard/android/apk/cobalt-gradle.sh',
+        '--sdk', '<(ANDROID_HOME)',
+        '--ndk', '<(NDK_HOME)',
+        '--cache', '<(GRADLE_FILES_DIR)/cache',
+        '--project-dir', '<(DEPTH)/starboard/android/apk/',
+        '-P', 'cobaltBuildAbi=<(ANDROID_ABI)',
+        '-P', 'cobaltDeployApk=<(apk)',
+        '-P', 'cobaltContentDir=<(content_deploy_dir)',
+        '-P', 'cobaltGradleDir=<(GRADLE_FILES_DIR)',
+        '-P', 'cobaltProductDir=<(PRODUCT_DIR)',
+        '-P', 'cobaltTarget=<(executable_name)',
+        'assembleCobalt_<(GRADLE_BUILD_TYPE)',
+      ],
+      'message': 'Build APK: <(apk)',
+    },
+    {
+      # Clean the gradle directory to conserve space after we have the APK
+      'action_name': 'delete_gradle_dir',
+      'inputs': [ '<(apk)' ],
+      'outputs': [ '<(GRADLE_FILES_DIR).deleted.stamp' ],
+      'action': [
+        'sh', '-c',
+        'rm -rf <(GRADLE_FILES_DIR) && touch <(GRADLE_FILES_DIR).deleted.stamp',
+      ],
+      'message': 'Cleanup Gradle: <(executable_name)',
+    },
+  ],
+}
diff --git a/src/starboard/android/shared/player_components_impl.cc b/src/starboard/android/shared/player_components_impl.cc
new file mode 100644
index 0000000..0d14113
--- /dev/null
+++ b/src/starboard/android/shared/player_components_impl.cc
@@ -0,0 +1,109 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/player/filter/player_components.h"
+
+#include "starboard/android/shared/audio_decoder.h"
+#include "starboard/android/shared/video_decoder.h"
+#include "starboard/android/shared/video_render_algorithm.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/media.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_renderer_sink.h"
+#include "starboard/shared/starboard/player/filter/audio_renderer_sink_impl.h"
+#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/video_render_algorithm.h"
+#include "starboard/shared/starboard/player/filter/video_render_algorithm_impl.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_sink.h"
+
+namespace starboard {
+namespace shared {
+namespace starboard {
+namespace player {
+namespace filter {
+
+namespace {
+
+class PlayerComponentsImpl : public PlayerComponents {
+  void CreateAudioComponents(
+      const AudioParameters& audio_parameters,
+      scoped_ptr<AudioDecoder>* audio_decoder,
+      scoped_ptr<AudioRendererSink>* audio_renderer_sink) override {
+    using AudioDecoderImpl = ::starboard::android::shared::AudioDecoder;
+
+    SB_DCHECK(audio_decoder);
+    SB_DCHECK(audio_renderer_sink);
+
+    scoped_ptr<AudioDecoderImpl> audio_decoder_impl(new AudioDecoderImpl(
+        audio_parameters.audio_codec, audio_parameters.audio_header,
+        audio_parameters.drm_system));
+    if (audio_decoder_impl->is_valid()) {
+      audio_decoder->reset(audio_decoder_impl.release());
+    } else {
+      audio_decoder->reset();
+    }
+    audio_renderer_sink->reset(new AudioRendererSinkImpl);
+  }
+
+  void CreateVideoComponents(
+      const VideoParameters& video_parameters,
+      scoped_ptr<VideoDecoder>* video_decoder,
+      scoped_ptr<VideoRenderAlgorithm>* video_render_algorithm,
+      scoped_refptr<VideoRendererSink>* video_renderer_sink) override {
+    using VideoDecoderImpl = ::starboard::android::shared::VideoDecoder;
+    using VideoRenderAlgorithmImpl =
+        ::starboard::android::shared::VideoRenderAlgorithm;
+
+    SB_DCHECK(video_decoder);
+    SB_DCHECK(video_render_algorithm);
+    SB_DCHECK(video_renderer_sink);
+
+    scoped_ptr<VideoDecoderImpl> video_decoder_impl(new VideoDecoderImpl(
+        video_parameters.video_codec, video_parameters.drm_system,
+        video_parameters.output_mode,
+        video_parameters.decode_target_graphics_context_provider));
+    if (video_decoder_impl->is_valid()) {
+      *video_renderer_sink = video_decoder_impl->GetSink();
+      video_decoder->reset(video_decoder_impl.release());
+    } else {
+      video_decoder->reset();
+      *video_renderer_sink = NULL;
+    }
+
+    video_render_algorithm->reset(new VideoRenderAlgorithmImpl);
+  }
+
+  void GetAudioRendererParams(int* max_cached_frames,
+                              int* max_frames_per_append) const override {
+    SB_DCHECK(max_cached_frames);
+    SB_DCHECK(max_frames_per_append);
+
+    *max_cached_frames = 256 * 1024;
+    *max_frames_per_append = 16384;
+  }
+};
+
+}  // namespace
+
+// static
+scoped_ptr<PlayerComponents> PlayerComponents::Create() {
+  return make_scoped_ptr<PlayerComponents>(new PlayerComponentsImpl);
+}
+
+}  // namespace filter
+}  // namespace player
+}  // namespace starboard
+}  // namespace shared
+}  // namespace starboard
diff --git a/src/starboard/android/shared/player_create.cc b/src/starboard/android/shared/player_create.cc
new file mode 100644
index 0000000..c305fe8
--- /dev/null
+++ b/src/starboard/android/shared/player_create.cc
@@ -0,0 +1,116 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/configuration.h"
+#include "starboard/decode_target.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+#include "starboard/shared/starboard/player/player_worker.h"
+
+using starboard::shared::starboard::player::filter::
+    FilterBasedPlayerWorkerHandler;
+using starboard::shared::starboard::player::PlayerWorker;
+using starboard::android::shared::cobalt::kPlaying;
+using starboard::android::shared::cobalt::
+    UpdateActiveSessionPlatformPlaybackState;
+using starboard::android::shared::cobalt::UpdateActiveSessionPlatformPlayer;
+
+SbPlayer SbPlayerCreate(SbWindow window,
+                        SbMediaVideoCodec video_codec,
+                        SbMediaAudioCodec audio_codec,
+#if SB_API_VERSION < 10
+                        SbMediaTime duration_pts,
+#endif  // SB_API_VERSION < 10
+                        SbDrmSystem drm_system,
+                        const SbMediaAudioHeader* audio_header,
+                        SbPlayerDeallocateSampleFunc sample_deallocate_func,
+                        SbPlayerDecoderStatusFunc decoder_status_func,
+                        SbPlayerStatusFunc player_status_func,
+                        SbPlayerErrorFunc player_error_func,
+                        void* context,
+                        SbPlayerOutputMode output_mode,
+                        SbDecodeTargetGraphicsContextProvider* provider) {
+  SB_UNREFERENCED_PARAMETER(window);
+  SB_UNREFERENCED_PARAMETER(provider);
+#if SB_API_VERSION < 10
+  SB_UNREFERENCED_PARAMETER(duration_pts);
+#endif  // SB_API_VERSION < 10
+
+  if (!sample_deallocate_func || !decoder_status_func || !player_status_func
+#if SB_HAS(PLAYER_ERROR_MESSAGE)
+      || !player_error_func
+#endif  // SB_HAS(PLAYER_ERROR_MESSAGE)
+      ) {
+    return kSbPlayerInvalid;
+  }
+
+  if (audio_codec != kSbMediaAudioCodecNone &&
+      audio_codec != kSbMediaAudioCodecAac) {
+    SB_LOG(ERROR) << "Unsupported audio codec " << audio_codec;
+    return kSbPlayerInvalid;
+  }
+
+  if (audio_codec != kSbMediaAudioCodecNone && !audio_header) {
+    SB_LOG(ERROR) << "SbPlayerCreate() requires a non-NULL SbMediaAudioHeader "
+                  << "when |audio_codec| is not kSbMediaAudioCodecNone";
+    return kSbPlayerInvalid;
+  }
+
+  if (video_codec != kSbMediaVideoCodecNone &&
+      video_codec != kSbMediaVideoCodecH264 &&
+      video_codec != kSbMediaVideoCodecVp9) {
+    SB_LOG(ERROR) << "Unsupported video codec " << video_codec;
+    return kSbPlayerInvalid;
+  }
+
+  if (audio_codec == kSbMediaAudioCodecNone &&
+      video_codec == kSbMediaVideoCodecNone) {
+    SB_LOG(ERROR) << "SbPlayerCreate() requires at least one audio track or"
+                  << " one video track.";
+    return kSbPlayerInvalid;
+  }
+
+  if (!SbPlayerOutputModeSupported(output_mode, video_codec, drm_system)) {
+    SB_LOG(ERROR) << "Unsupported player output mode " << output_mode;
+    return kSbPlayerInvalid;
+  }
+
+  // TODO: increase this once we support multiple video windows.
+  const int kMaxNumberOfPlayers = 1;
+  if (SbPlayerPrivate::number_of_players() >= kMaxNumberOfPlayers) {
+    return kSbPlayerInvalid;
+  }
+
+  UpdateActiveSessionPlatformPlaybackState(kPlaying);
+
+  starboard::scoped_ptr<PlayerWorker::Handler> handler(
+      new FilterBasedPlayerWorkerHandler(video_codec, audio_codec, drm_system,
+                                         audio_header, output_mode, provider));
+  SbPlayer player = new SbPlayerPrivate(
+      audio_codec, video_codec, sample_deallocate_func, decoder_status_func,
+      player_status_func, player_error_func, context, handler.Pass());
+
+  // TODO: accomplish this through more direct means.
+  // Set the bounds to initialize the VideoSurfaceView. The initial values don't
+  // matter.
+  SbPlayerSetBounds(player, 0, 0, 0, 0, 0);
+
+  UpdateActiveSessionPlatformPlayer(player);
+
+  return player;
+}
diff --git a/src/starboard/android/shared/player_destroy.cc b/src/starboard/android/shared/player_destroy.cc
new file mode 100644
index 0000000..b1b5a96
--- /dev/null
+++ b/src/starboard/android/shared/player_destroy.cc
@@ -0,0 +1,32 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+using starboard::android::shared::cobalt::kNone;
+using starboard::android::shared::cobalt::
+    UpdateActiveSessionPlatformPlaybackState;
+using starboard::android::shared::cobalt::UpdateActiveSessionPlatformPlayer;
+
+void SbPlayerDestroy(SbPlayer player) {
+  if (!SbPlayerIsValid(player)) {
+    return;
+  }
+  UpdateActiveSessionPlatformPlaybackState(kNone);
+  UpdateActiveSessionPlatformPlayer(kSbPlayerInvalid);
+  delete player;
+}
diff --git a/src/starboard/android/shared/player_set_bounds.cc b/src/starboard/android/shared/player_set_bounds.cc
new file mode 100644
index 0000000..d783a66
--- /dev/null
+++ b/src/starboard/android/shared/player_set_bounds.cc
@@ -0,0 +1,34 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+void SbPlayerSetBounds(SbPlayer player,
+                       int z_index,
+                       int x,
+                       int y,
+                       int width,
+                       int height) {
+  if (!SbPlayerIsValid(player)) {
+    SB_DLOG(WARNING) << "player is invalid.";
+    return;
+  }
+  starboard::android::shared::JniEnvExt::Get()->CallStarboardVoidMethodOrAbort(
+      "setVideoSurfaceBounds", "(IIII)V", x, y, width, height);
+  player->SetBounds(z_index, x, y, width, height);
+}
diff --git a/src/starboard/android/shared/player_set_playback_rate.cc b/src/starboard/android/shared/player_set_playback_rate.cc
new file mode 100644
index 0000000..413c093
--- /dev/null
+++ b/src/starboard/android/shared/player_set_playback_rate.cc
@@ -0,0 +1,41 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/player.h"
+
+#include "starboard/android/shared/cobalt/android_media_session_client.h"
+#include "starboard/log.h"
+#include "starboard/shared/starboard/player/player_internal.h"
+
+using starboard::android::shared::cobalt::kPaused;
+using starboard::android::shared::cobalt::kPlaying;
+using starboard::android::shared::cobalt::
+    UpdateActiveSessionPlatformPlaybackState;
+
+bool SbPlayerSetPlaybackRate(SbPlayer player, double playback_rate) {
+  if (!SbPlayerIsValid(player)) {
+    SB_DLOG(WARNING) << "player is invalid.";
+    return false;
+  }
+  if (playback_rate < 0.0) {
+    SB_DLOG(WARNING) << "playback_rate cannot be negative but it is set to "
+                     << playback_rate << '.';
+    return false;
+  }
+  bool paused = (playback_rate == 0.0);
+  UpdateActiveSessionPlatformPlaybackState(paused ? kPaused : kPlaying);
+
+  player->SetPlaybackRate(playback_rate);
+  return true;
+}
diff --git a/src/starboard/android/shared/sanitizer_options.cc b/src/starboard/android/shared/sanitizer_options.cc
new file mode 100644
index 0000000..aafbc54
--- /dev/null
+++ b/src/starboard/android/shared/sanitizer_options.cc
@@ -0,0 +1,42 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Removes gallium leak warnings from x11 GL code, but defines it as a weak
+// symbol, so other code can override it if they want to.
+
+#if defined(ADDRESS_SANITIZER)
+
+// Functions returning default options are declared weak in the tools' runtime
+// libraries. To make the linker pick the strong replacements for those
+// functions from this module, we explicitly force its inclusion by passing
+// -Wl,-u_sanitizer_options_link_helper
+extern "C" void _sanitizer_options_link_helper() { }
+
+#define SANITIZER_HOOK_ATTRIBUTE          \
+  extern "C"                              \
+  __attribute__((no_sanitize_address))    \
+  __attribute__((no_sanitize_memory))     \
+  __attribute__((no_sanitize_thread))     \
+  __attribute__((visibility("default")))  \
+  __attribute__((weak))                   \
+  __attribute__((used))
+
+// Newline separated list of issues to suppress, see
+// http://clang.llvm.org/docs/AddressSanitizer.html#issue-suppression
+// http://llvm.org/svn/llvm-project/compiler-rt/trunk/lib/sanitizer_common/sanitizer_suppressions.cc
+SANITIZER_HOOK_ATTRIBUTE const char* __lsan_default_suppressions() {
+  return "leak:egl_gallium.so\n";
+}
+
+#endif  // defined(ADDRESS_SANITIZER)
diff --git a/src/starboard/android/shared/sdk_utils.py b/src/starboard/android/shared/sdk_utils.py
new file mode 100644
index 0000000..64a67ce
--- /dev/null
+++ b/src/starboard/android/shared/sdk_utils.py
@@ -0,0 +1,359 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Utilities to use the toolchain from the Android NDK."""
+
+import ConfigParser
+import fcntl
+import hashlib
+import logging
+import os
+import re
+import shutil
+import StringIO
+import subprocess
+import sys
+import time
+import urllib
+import zipfile
+
+from starboard.tools import build
+
+# The API level of NDK standalone toolchain to install. This should be the
+# minimum API level on which the app is expected to run. If some feature from a
+# newer NDK level is needed, this may be increased with caution.
+# https://developer.android.com/ndk/guides/stable_apis.html
+#
+# Using 24 will lead to missing symbols on API 23 devices.
+# https://github.com/android-ndk/ndk/issues/126
+_ANDROID_NDK_API_LEVEL = '21'
+
+# Packages to install in the Android SDK.
+# Get available packages from "sdkmanager --list --verbose"
+_ANDROID_SDK_PACKAGES = [
+    'build-tools;28.0.3',
+    'cmake;3.6.4111459',
+    'emulator',
+    'extras;android;m2repository',
+    'extras;google;m2repository',
+    'lldb;3.1',
+    'ndk-bundle',
+    'patcher;v4',
+    'platforms;android-28',
+    'platform-tools',
+    'tools',
+]
+
+# Seconds to sleep before writing "y" for android sdk update license prompt.
+_SDK_LICENSE_PROMPT_SLEEP_SECONDS = 5
+
+# Location from which to download the SDK command-line tools
+# see https://developer.android.com/studio/index.html#command-tools
+_SDK_URL = 'https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip'
+
+_STARBOARD_TOOLCHAINS_DIR = build.GetToolchainsDir()
+
+# The path to the Android SDK, if placed inside of starboard-toolchains.
+_STARBOARD_TOOLCHAINS_SDK_DIR = os.path.join(_STARBOARD_TOOLCHAINS_DIR,
+                                             'AndroidSdk')
+
+_ANDROID_HOME = os.environ.get('ANDROID_HOME')
+if _ANDROID_HOME:
+  _SDK_PATH = _ANDROID_HOME
+else:
+  _SDK_PATH = _STARBOARD_TOOLCHAINS_SDK_DIR
+
+_ANDROID_NDK_HOME = os.environ.get('ANDROID_NDK_HOME')
+if _ANDROID_NDK_HOME:
+  _NDK_PATH = _ANDROID_NDK_HOME
+else:
+  _NDK_PATH = os.path.join(_SDK_PATH, 'ndk-bundle')
+
+_SDKMANAGER_TOOL = os.path.join(_SDK_PATH, 'tools', 'bin', 'sdkmanager')
+
+# Maps the Android ABI to the architecture name of the toolchain.
+_TOOLS_ABI_ARCH_MAP = {
+    'x86': 'x86',
+    'armeabi': 'arm',
+    'armeabi-v7a': 'arm',
+    'arm64-v8a': 'arm64',
+}
+
+_SCRIPT_HASH_PROPERTY = 'SdkUtils.Hash'
+
+with open(__file__, 'rb') as script:
+  _SCRIPT_HASH = hashlib.md5(script.read()).hexdigest()
+
+
+def GetToolsPath(abi):
+  """Returns the path where the NDK standalone toolchain should be."""
+  tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
+  tools_dir = 'android_toolchain_api{}_{}'.format(_ANDROID_NDK_API_LEVEL,
+                                                  tools_arch)
+  return os.path.realpath(os.path.join(_STARBOARD_TOOLCHAINS_DIR, tools_dir))
+
+
+def GetEnvironmentVariables(abi):
+  """Returns a dictionary of environment variables to provide to GYP."""
+  tools_bin = os.path.join(GetToolsPath(abi), 'bin')
+  return {
+      'CC': os.path.join(tools_bin, 'clang'),
+      'CXX': os.path.join(tools_bin, 'clang++'),
+  }
+
+
+def _CheckStamp(dir_path):
+  """Checks that the specified directory is up-to-date with the NDK."""
+  stamp_path = os.path.join(dir_path, 'ndk.stamp')
+  return (
+      os.path.exists(stamp_path) and
+      _ReadNdkRevision(stamp_path) == _GetInstalledNdkRevision() and
+      _ReadProperty(stamp_path, _SCRIPT_HASH_PROPERTY) == _SCRIPT_HASH)
+
+
+def _UpdateStamp(dir_path):
+  """Updates the stamp file in the specified directory to the NDK revision."""
+  path = GetNdkPath()
+  properties_path = os.path.join(path, 'source.properties')
+  stamp_path = os.path.join(dir_path, 'ndk.stamp')
+  shutil.copyfile(properties_path, stamp_path)
+  with open(stamp_path, 'a') as stamp:
+    stamp.write('{} = {}\n'.format(_SCRIPT_HASH_PROPERTY, _SCRIPT_HASH))
+
+
+def GetNdkPath():
+  return _NDK_PATH
+
+
+def GetSdkPath():
+  return _SDK_PATH
+
+
+def _ReadNdkRevision(properties_path):
+  return _ReadProperty(properties_path, 'pkg.revision')
+
+
+def _ReadProperty(properties_path, property_key):
+  with open(properties_path, 'r') as f:
+    ini_str = '[properties]\n' + f.read()
+  config = ConfigParser.RawConfigParser()
+  config.readfp(StringIO.StringIO(ini_str))
+  try:
+    return config.get('properties', property_key)
+  except ConfigParser.NoOptionError:
+    return None
+
+
+def _GetInstalledNdkRevision():
+  """Returns the installed NDK's revision."""
+  path = GetNdkPath()
+  properties_path = os.path.join(path, 'source.properties')
+  try:
+    return _ReadNdkRevision(properties_path)
+  except IOError:
+    logging.error("Error: Can't read NDK properties in %s", properties_path)
+    sys.exit(1)
+
+
+def InstallSdkIfNeeded(abi):
+  """Installs appropriate SDK/NDK and NDK standalone tools if needed."""
+  _MaybeDownloadAndInstallSdkAndNdk()
+  _MaybeMakeToolchain(abi)
+
+
+def _DownloadAndUnzipFile(url, destination_path):
+  dl_file, dummy_headers = urllib.urlretrieve(url)
+  _UnzipFile(dl_file, destination_path)
+
+
+def _UnzipFile(zip_path, dest_path):
+  """Extract all files and restore permissions from a zip file."""
+  zip_file = zipfile.ZipFile(zip_path)
+  for info in zip_file.infolist():
+    zip_file.extract(info.filename, dest_path)
+    os.chmod(os.path.join(dest_path, info.filename), info.external_attr >> 16L)
+
+
+def _MaybeDownloadAndInstallSdkAndNdk():
+  """Download the SDK and NDK if not already available."""
+  # Hold an exclusive advisory lock on the _STARBOARD_TOOLCHAINS_DIR, to
+  # prevent issues with modification for multiple variants.
+  try:
+    toolchains_dir_fd = os.open(_STARBOARD_TOOLCHAINS_DIR, os.O_RDONLY)
+    fcntl.flock(toolchains_dir_fd, fcntl.LOCK_EX)
+
+    if _ANDROID_HOME:
+      if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+        logging.error('Error: ANDROID_HOME is set but SDK is not present!')
+        sys.exit(1)
+      logging.warning('Warning: Using Android SDK in ANDROID_HOME,'
+                      ' which is not automatically updated.\n'
+                      '         The following package versions are installed:')
+      installed_packages = _GetInstalledSdkPackages()
+      for package in _ANDROID_SDK_PACKAGES:
+        version = installed_packages.get(package, '< MISSING! >')
+        msg = '  {:30} : {}'.format(package, version)
+        logging.warning(msg)
+    else:
+      logging.warning('Checking Android SDK.')
+      _DownloadInstallOrUpdateSdk()
+
+    if _ANDROID_NDK_HOME:
+      logging.warning('Warning: Using Android NDK in ANDROID_NDK_HOME,'
+                      ' which is not automatically updated')
+    ndk_revision = _GetInstalledNdkRevision()
+    logging.warning('Using Android NDK version %s', ndk_revision)
+
+    if _ANDROID_HOME or _ANDROID_NDK_HOME:
+      reply = raw_input(
+          'Do you want to continue using your custom Android tools? [yN]')
+      if reply.upper() != 'Y':
+        sys.exit(1)
+
+  finally:
+    fcntl.flock(toolchains_dir_fd, fcntl.LOCK_UN)
+    os.close(toolchains_dir_fd)
+
+
+def _GetInstalledSdkPackages():
+  """Returns a map of installed package name to package version."""
+  # We detect and parse either new-style or old-style output from sdkmanager:
+  #
+  # [new-style]
+  # Installed packages:
+  # --------------------------------------
+  # build-tools;25.0.2
+  #     Description:        Android SDK Build-Tools 25.0.2
+  #     Version:            25.0.2
+  #     Installed Location: <abs path>
+  # ...
+  #
+  # [old-style]
+  # Installed packages:
+  #   Path               | Version | Description                    | Location
+  #   -------            | ------- | -------                        | -------
+  #   build-tools;25.0.2 | 25.0.2  | Android SDK Build-Tools 25.0.2 | <rel path>
+  # ...
+  section_re = re.compile(r'^[A-Z][^:]*:$')
+  version_re = re.compile(r'^\s+Version:\s+(\S+)')
+
+  p = subprocess.Popen([_SDKMANAGER_TOOL, '--list', '--verbose'],
+                       stdout=subprocess.PIPE)
+
+  installed_package_versions = {}
+  new_style = False
+  old_style = False
+  for line in iter(p.stdout.readline, ''):
+
+    if section_re.match(line):
+      if new_style or old_style:
+        # We left the new/old style installed packages section
+        break
+      if line.startswith('Installed'):
+        # Read the header of this section to determine new/old style
+        line = p.stdout.readline().strip()
+        new_style = line.startswith('----')
+        old_style = line.startswith('Path')
+        if old_style:
+          line = p.stdout.readline().strip()
+        if not line.startswith('----'):
+          logging.error('Error: Unexpected SDK package listing format')
+      continue
+
+    # We're in the installed packages section, and it's new-style
+    if new_style:
+      if not line.startswith(' '):
+        pkg_name = line.strip()
+      else:
+        m = version_re.match(line)
+        if m:
+          installed_package_versions[pkg_name] = m.group(1)
+
+    # We're in the installed packages section, and it's old-style
+    elif old_style:
+      fields = [f.strip() for f in line.split('|', 2)]
+      if len(fields) >= 2:
+        installed_package_versions[fields[0]] = fields[1]
+
+  # Discard the rest of the output
+  p.communicate()
+  return installed_package_versions
+
+
+def _IsOnBuildbot():
+  return 'BUILDBOT_BUILDERNAME' in os.environ
+
+
+def _DownloadInstallOrUpdateSdk():
+  """Downloads (if necessary) and installs/updates the Android SDK."""
+
+  # If we can't access the "sdkmanager" tool, we need to download the SDK
+  if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+    logging.warning('Downloading Android SDK to %s',
+                    _STARBOARD_TOOLCHAINS_SDK_DIR)
+    if os.path.exists(_STARBOARD_TOOLCHAINS_SDK_DIR):
+      shutil.rmtree(_STARBOARD_TOOLCHAINS_SDK_DIR)
+    _DownloadAndUnzipFile(_SDK_URL, _STARBOARD_TOOLCHAINS_SDK_DIR)
+    if not os.access(_SDKMANAGER_TOOL, os.X_OK):
+      logging.error('SDK download failed.')
+      sys.exit(1)
+
+  # Run the "sdkmanager" command with the appropriate packages
+
+  if _IsOnBuildbot():
+    stdin = subprocess.PIPE
+  else:
+    stdin = sys.stdin
+
+  p = subprocess.Popen(
+      [_SDKMANAGER_TOOL, '--verbose'] + _ANDROID_SDK_PACKAGES,
+      stdin=stdin)
+
+  if _IsOnBuildbot():
+    time.sleep(_SDK_LICENSE_PROMPT_SLEEP_SECONDS)
+    try:
+      p.stdin.write('y\n')
+    except IOError:
+      logging.warning('There were no SDK licenses to accept.')
+
+  p.wait()
+
+
+def _MaybeMakeToolchain(abi):
+  """Run the NDK's make_standalone_toolchain.py if necessary."""
+  tools_arch = _TOOLS_ABI_ARCH_MAP[abi]
+  tools_path = GetToolsPath(abi)
+  if _CheckStamp(tools_path):
+    logging.info('NDK %s toolchain already at %s', tools_arch,
+                 _GetInstalledNdkRevision())
+    return
+
+  logging.warning('Installing NDK %s toolchain %s in %s', tools_arch,
+                  _GetInstalledNdkRevision(), tools_path)
+
+  if os.path.exists(tools_path):
+    shutil.rmtree(tools_path)
+
+  # Run the NDK script to make the standalone toolchain
+  script_path = os.path.join(GetNdkPath(), 'build', 'tools',
+                             'make_standalone_toolchain.py')
+  args = [
+      script_path, '--arch', tools_arch, '--api', _ANDROID_NDK_API_LEVEL,
+      '--stl', 'libc++', '--install-dir', tools_path
+  ]
+  script_proc = subprocess.Popen(args)
+  rc = script_proc.wait()
+  if rc != 0:
+    raise RuntimeError('%s failed.' % script_path)
+
+  _UpdateStamp(tools_path)
diff --git a/src/starboard/android/shared/socket_get_interface_address.cc b/src/starboard/android/shared/socket_get_interface_address.cc
new file mode 100644
index 0000000..0d76407
--- /dev/null
+++ b/src/starboard/android/shared/socket_get_interface_address.cc
@@ -0,0 +1,87 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "net/base/net_util.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/memory.h"
+#include "starboard/socket.h"
+
+using starboard::android::shared::JniEnvExt;
+
+namespace {
+
+const size_t kDefaultPrefixLength = 8;
+
+bool CopySocketAddress(jbyteArray array, SbSocketAddress* out_address) {
+  JniEnvExt* env = JniEnvExt::Get();
+  if (array == nullptr) {
+    return false;
+  }
+  if (out_address == nullptr) {
+    SB_LOG(ERROR) << "SbSocketGetInterfaceAddress NULL out_address";
+    return false;
+  }
+  jint size = env->GetArrayLength(array);
+  if (size > sizeof(out_address->address)) {
+    SB_LOG(ERROR) << "SbSocketGetInterfaceAddress address too long";
+    return false;
+  }
+  out_address->type =
+      (size == 4) ? kSbSocketAddressTypeIpv4 : kSbSocketAddressTypeIpv6;
+  jbyte* bytes = env->GetByteArrayElements(array, NULL);
+  SB_CHECK(bytes) << "GetByteArrayElements failed";
+  SbMemoryCopy(out_address->address, bytes, size);
+  env->ReleaseByteArrayElements(array, bytes, JNI_ABORT);
+  return true;
+}
+
+}  // namespace
+
+// Note: The following is an incorrect implementation for
+// SbSocketGetInterfaceAddress.  Specifically, things missing are:
+// We should see if the destination is NULL, or ANY address, or a regular
+// unicast address.
+// For IPv6, there are a few rules about which IPs are valid (and prefered).
+// E.g. globally routable IP addresses are prefered over ULAs.
+bool SbSocketGetInterfaceAddress(const SbSocketAddress* const destination,
+                                 SbSocketAddress* out_source_address,
+                                 SbSocketAddress* out_netmask) {
+  if (out_source_address == nullptr) {
+    return false;
+  }
+
+  SbMemorySet(out_source_address->address, 0,
+              sizeof(out_source_address->address));
+  out_source_address->port = 0;
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  jboolean want_ipv6 =
+      (destination != nullptr && destination->type == kSbSocketAddressTypeIpv6);
+  jobject pair = (jbyteArray)env->CallStarboardObjectMethodOrAbort(
+      "getLocalInterfaceAddressAndNetask", "(Z)Landroid/util/Pair;", want_ipv6);
+
+  jobject field;
+  field = env->GetObjectFieldOrAbort(pair, "first", "Ljava/lang/Object;");
+  if (!CopySocketAddress(static_cast<jbyteArray>(field), out_source_address)) {
+    return false;
+  }
+  field = env->GetObjectFieldOrAbort(pair, "second", "Ljava/lang/Object;");
+  if (out_netmask &&
+      !CopySocketAddress(static_cast<jbyteArray>(field), out_netmask)) {
+    return false;
+  }
+  return true;
+}
diff --git a/src/starboard/android/shared/speech_recognizer_impl.cc b/src/starboard/android/shared/speech_recognizer_impl.cc
new file mode 100644
index 0000000..bea9e52
--- /dev/null
+++ b/src/starboard/android/shared/speech_recognizer_impl.cc
@@ -0,0 +1,331 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/starboard/speech_recognizer/speech_recognizer_internal.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include <limits>
+#include <vector>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/mutex.h"
+#include "starboard/shared/starboard/thread_checker.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+// Android speech recognizer error.
+// Reference:
+// https://developer.android.com/reference/android/speech/SpeechRecognizer.html
+enum SpeechRecognizerErrorCode {
+  kErrorNetworkTimeout = 1,
+  kErrorNetwork = 2,
+  kErrorAudio = 3,
+  kErrorService = 4,
+  kErrorClient = 5,
+  kErrorSpeechTimeout = 6,
+  kErrorNoMatch = 7,
+  kErrorRecognizerBusy = 8,
+  kErrorInsufficientPermissions = 9,
+};
+}  // namespace
+
+class SbSpeechRecognizerImpl : public SbSpeechRecognizerPrivate {
+ public:
+  explicit SbSpeechRecognizerImpl(const SbSpeechRecognizerHandler* handler);
+  ~SbSpeechRecognizerImpl();
+
+  // Called from constructor's thread.
+  bool Start(const SbSpeechConfiguration* configuration) override;
+  void Stop() override;
+  void Cancel() override;
+
+  // Called from Android's UI thread.
+  void OnSpeechDetected(bool detected);
+  void OnError(int error);
+  void OnResults(const std::vector<std::string>& results,
+                 const std::vector<float>& confidences,
+                 bool is_final);
+
+ private:
+  SbSpeechRecognizerHandler handler_;
+  jobject j_voice_recognizer_;
+  bool is_started_;
+
+  ::starboard::shared::starboard::ThreadChecker thread_checker_;
+};
+
+SbSpeechRecognizerImpl::SbSpeechRecognizerImpl(
+    const SbSpeechRecognizerHandler* handler)
+    : handler_(*handler), is_started_(false) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject local_ref = env->CallStarboardObjectMethodOrAbort(
+      "getVoiceRecognizer", "()Ldev/cobalt/coat/VoiceRecognizer;");
+  j_voice_recognizer_ = env->ConvertLocalRefToGlobalRef(local_ref);
+}
+
+SbSpeechRecognizerImpl::~SbSpeechRecognizerImpl() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  Cancel();
+
+  JniEnvExt* env = JniEnvExt::Get();
+  env->DeleteGlobalRef(j_voice_recognizer_);
+}
+
+bool SbSpeechRecognizerImpl::Start(const SbSpeechConfiguration* configuration) {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+  SB_DCHECK(configuration);
+
+  if (is_started_) {
+    return false;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(
+      j_voice_recognizer_, "startRecognition", "(ZZIJ)V",
+      configuration->continuous, configuration->interim_results,
+      configuration->max_alternatives, reinterpret_cast<jlong>(this));
+
+  is_started_ = true;
+  return true;
+}
+
+void SbSpeechRecognizerImpl::Stop() {
+  SB_DCHECK(thread_checker_.CalledOnValidThread());
+
+  if (!is_started_) {
+    return;
+  }
+
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(j_voice_recognizer_, "stopRecognition", "()V");
+
+  is_started_ = false;
+}
+
+void SbSpeechRecognizerImpl::Cancel() {
+  Stop();
+}
+
+void SbSpeechRecognizerImpl::OnSpeechDetected(bool detected) {
+  // Called from Android's UI thread instead of constructor's thread.
+  SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+  handler_.on_speech_detected(handler_.context, detected);
+}
+
+void SbSpeechRecognizerImpl::OnError(int error) {
+  // Called from Android's UI thread instead of constructor's thread.
+  SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+  SbSpeechRecognizerError recognizer_error;
+  switch (error) {
+    case kErrorNetworkTimeout:
+      recognizer_error = kSbNetworkError;
+      break;
+    case kErrorNetwork:
+      recognizer_error = kSbNetworkError;
+      break;
+    case kErrorAudio:
+      recognizer_error = kSbAudioCaptureError;
+      break;
+    case kErrorService:
+      recognizer_error = kSbNetworkError;
+      break;
+    case kErrorClient:
+      recognizer_error = kSbAborted;
+      break;
+    case kErrorSpeechTimeout:
+      recognizer_error = kSbNoSpeechError;
+      break;
+    case kErrorRecognizerBusy:
+    case kErrorInsufficientPermissions:
+      recognizer_error = kSbNotAllowed;
+      break;
+    case kErrorNoMatch:
+      // Maybe keep listening until found a match or time-out triggered by
+      // client.
+      recognizer_error = kSbNoSpeechError;
+      break;
+  }
+  handler_.on_error(handler_.context, recognizer_error);
+}
+
+void SbSpeechRecognizerImpl::OnResults(const std::vector<std::string>& results,
+                                       const std::vector<float>& confidences,
+                                       bool is_final) {
+  // Called from Android's UI thread instead of constructor's thread.
+  SB_DCHECK(!thread_checker_.CalledOnValidThread());
+
+  bool has_confidence = (confidences.size() != 0);
+  if (has_confidence) {
+    SB_DCHECK(confidences.size() == results.size());
+  }
+  int kSpeechResultSize = results.size();
+  std::vector<SbSpeechResult> speech_results(kSpeechResultSize);
+  for (int i = 0; i < kSpeechResultSize; ++i) {
+    // The callback is responsible for freeing the buffer with
+    // SbMemoryDeallocate.
+    speech_results[i].transcript = SbStringDuplicate(results[i].c_str());
+    speech_results[i].confidence =
+        has_confidence ? confidences[i]
+                       : std::numeric_limits<float>::quiet_NaN();
+  }
+  handler_.on_results(handler_.context, speech_results.data(),
+                      kSpeechResultSize, is_final);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+namespace {
+
+using starboard::android::shared::JniEnvExt;
+
+starboard::Mutex s_speech_recognizer_mutex_;
+SbSpeechRecognizer s_speech_recognizer = kSbSpeechRecognizerInvalid;
+}  // namespace
+
+// static
+SbSpeechRecognizer SbSpeechRecognizerPrivate::CreateSpeechRecognizer(
+    const SbSpeechRecognizerHandler* handler) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  SB_DCHECK(!SbSpeechRecognizerIsValid(s_speech_recognizer));
+  s_speech_recognizer =
+      new starboard::android::shared::SbSpeechRecognizerImpl(handler);
+  return s_speech_recognizer;
+}
+
+// static
+void SbSpeechRecognizerPrivate::DestroySpeechRecognizer(
+    SbSpeechRecognizer speech_recognizer) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  SB_DCHECK(s_speech_recognizer == speech_recognizer);
+  SB_DCHECK(SbSpeechRecognizerIsValid(s_speech_recognizer));
+  delete s_speech_recognizer;
+  s_speech_recognizer = kSbSpeechRecognizerInvalid;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnSpeechDetected(
+    JNIEnv* env,
+    jobject jcaller,
+    jlong nativeSpeechRecognizerImpl,
+    jboolean detected) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  starboard::android::shared::SbSpeechRecognizerImpl* native =
+      reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+          nativeSpeechRecognizerImpl);
+  // This is called by the Android UI thread and it is possible that the
+  // SbSpeechRecognizer is destroyed before this is called.
+  if (native != s_speech_recognizer) {
+    SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+    return;
+  }
+
+  native->OnSpeechDetected(detected);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnError(
+    JNIEnv* env,
+    jobject jcaller,
+    jlong nativeSpeechRecognizerImpl,
+    jint error) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  starboard::android::shared::SbSpeechRecognizerImpl* native =
+      reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+          nativeSpeechRecognizerImpl);
+  // This is called by the Android UI thread and it is possible that the
+  // SbSpeechRecognizer is destroyed before this is called.
+  if (native != s_speech_recognizer) {
+    SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+    return;
+  }
+
+  native->OnError(error);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeHandlePermission(
+    JNIEnv* env,
+    jobject jcaller,
+    jlong nativeSpeechRecognizerImpl,
+    jboolean is_granted) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  starboard::android::shared::SbSpeechRecognizerImpl* native =
+      reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+          nativeSpeechRecognizerImpl);
+  // This is called by the Android UI thread and it is possible that the
+  // SbSpeechRecognizer is destroyed before this is called.
+  if (native != s_speech_recognizer) {
+    SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+    return;
+  }
+  if (!is_granted) {
+    native->OnError(starboard::android::shared::kErrorInsufficientPermissions);
+  }
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_VoiceRecognizer_nativeOnResults(
+    JniEnvExt* env,
+    jobject unused_this,
+    jlong nativeSpeechRecognizerImpl,
+    jobjectArray results,
+    jfloatArray confidences,
+    jboolean is_final) {
+  starboard::ScopedLock lock(s_speech_recognizer_mutex_);
+
+  starboard::android::shared::SbSpeechRecognizerImpl* native =
+      reinterpret_cast<starboard::android::shared::SbSpeechRecognizerImpl*>(
+          nativeSpeechRecognizerImpl);
+  // This is called by the Android UI thread and it is possible that the
+  // SbSpeechRecognizer is destroyed before this is called.
+  if (native != s_speech_recognizer) {
+    SB_DLOG(WARNING) << "The speech recognizer is destroyed.";
+    return;
+  }
+
+  std::vector<std::string> options;
+  jint argc = env->GetArrayLength(results);
+  for (jint i = 0; i < argc; i++) {
+    starboard::android::shared::ScopedLocalJavaRef<jstring> element(
+        env->GetObjectArrayElement(results, i));
+    std::string utf_str = env->GetStringStandardUTFOrAbort(element.Get());
+    options.push_back(utf_str);
+  }
+
+  std::vector<float> scores(options.size(), 0.0);
+  if (confidences != NULL) {
+    SB_DCHECK(argc == env->GetArrayLength(results));
+    float* confidences_array = env->GetFloatArrayElements(confidences, NULL);
+    std::copy(confidences_array, confidences_array + argc, scores.begin());
+    env->ReleaseFloatArrayElements(confidences, confidences_array, 0);
+  }
+  native->OnResults(options, scores, is_final);
+}
diff --git a/src/starboard/android/shared/speech_synthesis_cancel.cc b/src/starboard/android/shared/speech_synthesis_cancel.cc
new file mode 100644
index 0000000..6eab70e
--- /dev/null
+++ b/src/starboard/android/shared/speech_synthesis_cancel.cc
@@ -0,0 +1,31 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/speech_synthesis.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+void SbSpeechSynthesisCancel() {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  ScopedLocalJavaRef<jobject> j_tts_helper(
+      env->CallStarboardObjectMethodOrAbort(
+          "getTextToSpeechHelper",
+          "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+  env->CallVoidMethodOrAbort(j_tts_helper.Get(), "cancel", "()V");
+}
diff --git a/src/starboard/android/shared/speech_synthesis_speak.cc b/src/starboard/android/shared/speech_synthesis_speak.cc
new file mode 100644
index 0000000..b571951
--- /dev/null
+++ b/src/starboard/android/shared/speech_synthesis_speak.cc
@@ -0,0 +1,33 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/speech_synthesis.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+void SbSpeechSynthesisSpeak(const char* text) {
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_tts_helper(
+      env->CallStarboardObjectMethodOrAbort(
+          "getTextToSpeechHelper",
+          "()Ldev/cobalt/coat/CobaltTextToSpeechHelper;"));
+  ScopedLocalJavaRef<jstring> j_text_string(
+      env->NewStringStandardUTFOrAbort(text));
+  env->CallVoidMethodOrAbort(j_tts_helper.Get(),
+      "speak", "(Ljava/lang/String;)V", j_text_string.Get());
+}
diff --git a/src/starboard/android/shared/starboard_platform.gypi b/src/starboard/android/shared/starboard_platform.gypi
new file mode 100644
index 0000000..f4b70e5
--- /dev/null
+++ b/src/starboard/android/shared/starboard_platform.gypi
@@ -0,0 +1,472 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'variables': {
+    'has_input_events_filter' : '<!(python ../../../build/file_exists.py <(DEPTH)/starboard/android/shared/input_events_filter.cc)',
+  },
+  'includes': [
+    '<(DEPTH)/starboard/shared/starboard/player/filter/player_filter.gypi',
+  ],
+  'targets': [
+    {
+      'target_name': 'starboard_base_symbolize',
+      'type': 'static_library',
+      'sources': [
+        '<(DEPTH)/base/third_party/symbolize/demangle.cc',
+        '<(DEPTH)/base/third_party/symbolize/symbolize.cc',
+      ],
+    },
+    # Copy sources that we compile from the NDK so that we can reference them
+    # by a relative path. Otherwise, without GYP pathname relativization
+    # different configuration builds would clobber each other since they'd all
+    # generate their .o at the same path in the NDK sources directory.
+    {
+      'target_name': 'ndk_sources',
+      'type': 'none',
+      'copies': [{
+        'destination': '<(SHARED_INTERMEDIATE_DIR)/ndk-sources/',
+        'files': [
+          '<(NDK_HOME)/sources/android/cpufeatures/cpu-features.c',
+        ],
+      }],
+    },
+    {
+      'target_name': 'starboard_platform',
+      'type': 'static_library',
+      'sources': [
+        '<@(filter_based_player_sources)',
+        'accessibility_get_caption_settings.cc',
+        'accessibility_get_display_settings.cc',
+        'accessibility_get_text_to_speech_settings.cc',
+        'accessibility_set_captions_enabled.cc',
+        'android_main.cc',
+        'application_android.cc',
+        'application_android.h',
+        'atomic_public.h',
+        'audio_decoder.cc',
+        'audio_decoder.h',
+        'audio_renderer.h',
+        'audio_sink_get_max_channels.cc',
+        'audio_sink_get_nearest_supported_sample_frequency.cc',
+        'audio_sink_is_audio_frame_storage_type_supported.cc',
+        'audio_sink_is_audio_sample_type_supported.cc',
+        'audio_track_audio_sink_type.cc',
+        'audio_track_audio_sink_type.h',
+        'configuration_public.h',
+        'decode_target_create.cc',
+        'decode_target_create.h',
+        'decode_target_get_info.cc',
+        'decode_target_internal.cc',
+        'decode_target_internal.h',
+        'decode_target_release.cc',
+        'directory_close.cc',
+        'directory_get_next.cc',
+        'directory_internal.h',
+        'directory_open.cc',
+        'drm_create_system.cc',
+        'drm_system.cc',
+        'drm_system.h',
+        'egl_swap_buffers.cc',
+        'file_can_open.cc',
+        'file_close.cc',
+        'file_delete.cc',
+        'file_exists.cc',
+        'file_flush.cc',
+        'file_get_info.cc',
+        'file_get_path_info.cc',
+        'file_internal.cc',
+        'file_internal.h',
+        'file_open.cc',
+        'file_read.cc',
+        'file_seek.cc',
+        'file_truncate.cc',
+        'file_write.cc',
+        'get_home_directory.cc',
+        'input_events_generator.cc',
+        'input_events_generator.h',
+        'jni_env_ext.cc',
+        'jni_env_ext.h',
+        'jni_utils.h',
+        'log.cc',
+        'log_flush.cc',
+        'log_format.cc',
+        'log_internal.h',
+        'log_is_tty.cc',
+        'log_raw.cc',
+        'main.cc',
+        'media_codec_bridge.cc',
+        'media_codec_bridge.h',
+        'media_common.h',
+        'media_decoder.cc',
+        'media_decoder.h',
+        'media_get_audio_configuration.cc',
+        'media_get_audio_output_count.cc',
+        'media_get_initial_buffer_capacity.cc',
+        'media_get_max_buffer_capacity.cc',
+        'media_is_audio_supported.cc',
+        'media_is_output_protected.cc',
+        'media_is_supported.cc',
+        'media_is_video_supported.cc',
+        'media_set_output_protection.cc',
+        'microphone_impl.cc',
+        'player_components_impl.cc',
+        'player_create.cc',
+        'player_destroy.cc',
+        'player_set_bounds.cc',
+        'player_set_playback_rate.cc',
+        'sanitizer_options.cc',
+        'socket_get_interface_address.cc',
+        'speech_recognizer_impl.cc',
+        'speech_synthesis_cancel.cc',
+        'speech_synthesis_speak.cc',
+        'system_get_connection_type.cc',
+        'system_get_device_type.cc',
+        'system_get_locale_id.cc',
+        'system_get_path.cc',
+        'system_get_property.cc',
+        'system_get_stack.cc',
+        'system_has_capability.cc',
+        'system_platform_error.cc',
+        'system_request_stop.cc',
+        'system_request_suspend.cc',
+        'thread_create.cc',
+        'thread_create_priority.cc',
+        'thread_get_name.cc',
+        'thread_types_public.h',
+        'time_zone_get_dst_name.cc',
+        'time_zone_get_name.cc',
+        'trace_util.h',
+        'video_decoder.cc',
+        'video_decoder.h',
+        'video_render_algorithm.cc',
+        'video_render_algorithm.h',
+        'video_window.cc',
+        'video_window.h',
+        'window_create.cc',
+        'window_destroy.cc',
+        'window_get_platform_handle.cc',
+        'window_get_size.cc',
+        'window_internal.h',
+        '<(SHARED_INTERMEDIATE_DIR)/ndk-sources/cpu-features.c',
+        '<(DEPTH)/starboard/accessibility.h',
+        '<(DEPTH)/starboard/atomic.h',
+        '<(DEPTH)/starboard/audio_sink.h',
+        '<(DEPTH)/starboard/common/ref_counted.h',
+        '<(DEPTH)/starboard/common/scoped_ptr.h',
+        '<(DEPTH)/starboard/condition_variable.h',
+        '<(DEPTH)/starboard/configuration.h',
+        '<(DEPTH)/starboard/decode_target.h',
+        '<(DEPTH)/starboard/directory.h',
+        '<(DEPTH)/starboard/drm.h',
+        '<(DEPTH)/starboard/event.h',
+        '<(DEPTH)/starboard/export.h',
+        '<(DEPTH)/starboard/file.h',
+        '<(DEPTH)/starboard/input.h',
+        '<(DEPTH)/starboard/key.h',
+        '<(DEPTH)/starboard/log.h',
+        '<(DEPTH)/starboard/media.h',
+        '<(DEPTH)/starboard/memory.h',
+        '<(DEPTH)/starboard/mutex.h',
+        '<(DEPTH)/starboard/once.h',
+        '<(DEPTH)/starboard/player.h',
+        '<(DEPTH)/starboard/queue.h',
+        '<(DEPTH)/starboard/socket.h',
+        '<(DEPTH)/starboard/speech_synthesis.h',
+        '<(DEPTH)/starboard/string.h',
+        '<(DEPTH)/starboard/system.h',
+        '<(DEPTH)/starboard/thread.h',
+        '<(DEPTH)/starboard/time_zone.h',
+        '<(DEPTH)/starboard/types.h',
+        '<(DEPTH)/starboard/window.h',
+        '<(DEPTH)/starboard/shared/dlmalloc/memory_map.cc',
+        '<(DEPTH)/starboard/shared/dlmalloc/memory_protect.cc',
+        '<(DEPTH)/starboard/shared/dlmalloc/memory_unmap.cc',
+        '<(DEPTH)/starboard/shared/gcc/atomic_gcc_public.h',
+        '<(DEPTH)/starboard/shared/gles/gl_call.h',
+        '<(DEPTH)/starboard/shared/internal_only.h',
+        '<(DEPTH)/starboard/shared/iso/character_is_alphanumeric.cc',
+        '<(DEPTH)/starboard/shared/iso/character_is_digit.cc',
+        '<(DEPTH)/starboard/shared/iso/character_is_hex_digit.cc',
+        '<(DEPTH)/starboard/shared/iso/character_is_space.cc',
+        '<(DEPTH)/starboard/shared/iso/character_is_upper.cc',
+        '<(DEPTH)/starboard/shared/iso/character_to_lower.cc',
+        '<(DEPTH)/starboard/shared/iso/character_to_upper.cc',
+        '<(DEPTH)/starboard/shared/iso/double_absolute.cc',
+        '<(DEPTH)/starboard/shared/iso/double_exponent.cc',
+        '<(DEPTH)/starboard/shared/iso/double_floor.cc',
+        '<(DEPTH)/starboard/shared/iso/double_is_finite.cc',
+        '<(DEPTH)/starboard/shared/iso/double_is_nan.cc',
+        '<(DEPTH)/starboard/shared/iso/impl/directory_close.h',
+        '<(DEPTH)/starboard/shared/iso/impl/directory_get_next.h',
+        '<(DEPTH)/starboard/shared/iso/impl/directory_open.h',
+        '<(DEPTH)/starboard/shared/iso/memory_allocate_unchecked.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_compare.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_copy.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_find_byte.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_free.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_move.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_reallocate_unchecked.cc',
+        '<(DEPTH)/starboard/shared/iso/memory_set.cc',
+        '<(DEPTH)/starboard/shared/iso/string_compare.cc',
+        '<(DEPTH)/starboard/shared/iso/string_compare_all.cc',
+        '<(DEPTH)/starboard/shared/iso/string_find_character.cc',
+        '<(DEPTH)/starboard/shared/iso/string_find_last_character.cc',
+        '<(DEPTH)/starboard/shared/iso/string_find_string.cc',
+        '<(DEPTH)/starboard/shared/iso/string_get_length.cc',
+        '<(DEPTH)/starboard/shared/iso/string_get_length_wide.cc',
+        '<(DEPTH)/starboard/shared/iso/string_parse_double.cc',
+        '<(DEPTH)/starboard/shared/iso/string_parse_signed_integer.cc',
+        '<(DEPTH)/starboard/shared/iso/string_parse_uint64.cc',
+        '<(DEPTH)/starboard/shared/iso/string_parse_unsigned_integer.cc',
+        '<(DEPTH)/starboard/shared/iso/string_scan.cc',
+        '<(DEPTH)/starboard/shared/iso/system_binary_search.cc',
+        '<(DEPTH)/starboard/shared/iso/system_sort.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_add.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_create.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_destroy.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_internal.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_remove.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_wait.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_wait_timed.cc',
+        '<(DEPTH)/starboard/shared/libevent/socket_waiter_wake_up.cc',
+        '<(DEPTH)/starboard/shared/linux/byte_swap.cc',
+        '<(DEPTH)/starboard/shared/linux/memory_get_stack_bounds.cc',
+        '<(DEPTH)/starboard/shared/linux/page_internal.cc',
+        '<(DEPTH)/starboard/shared/linux/system_get_random_data.cc',
+        '<(DEPTH)/starboard/shared/linux/system_get_total_cpu_memory.cc',
+        '<(DEPTH)/starboard/shared/linux/system_get_used_cpu_memory.cc',
+        '<(DEPTH)/starboard/shared/linux/system_is_debugger_attached.cc',
+        '<(DEPTH)/starboard/shared/linux/system_symbolize.cc',
+        '<(DEPTH)/starboard/shared/linux/thread_get_id.cc',
+        '<(DEPTH)/starboard/shared/linux/thread_set_name.cc',
+        '<(DEPTH)/starboard/shared/nouser/user_get_current.cc',
+        '<(DEPTH)/starboard/shared/nouser/user_get_property.cc',
+        '<(DEPTH)/starboard/shared/nouser/user_get_signed_in.cc',
+        '<(DEPTH)/starboard/shared/nouser/user_internal.cc',
+        '<(DEPTH)/starboard/shared/nouser/user_internal.h',
+        '<(DEPTH)/starboard/shared/posix/directory_create.cc',
+        '<(DEPTH)/starboard/shared/posix/impl/file_can_open.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_close.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_delete.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_flush.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_get_info.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_get_path_info.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_open.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_read.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_seek.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_truncate.h',
+        '<(DEPTH)/starboard/shared/posix/impl/file_write.h',
+        '<(DEPTH)/starboard/shared/posix/memory_allocate_aligned_unchecked.cc',
+        '<(DEPTH)/starboard/shared/posix/memory_flush.cc',
+        '<(DEPTH)/starboard/shared/posix/memory_free_aligned.cc',
+        '<(DEPTH)/starboard/shared/posix/set_non_blocking_internal.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_accept.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_bind.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_clear_last_error.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_connect.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_create.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_destroy.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_free_resolution.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_get_last_error.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_get_local_address.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_internal.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_is_connected.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_is_connected_and_idle.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_join_multicast_group.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_listen.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_receive_from.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_resolve.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_send_to.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_broadcast.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_receive_buffer_size.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_reuse_address.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_send_buffer_size.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_tcp_keep_alive.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_tcp_no_delay.cc',
+        '<(DEPTH)/starboard/shared/posix/socket_set_tcp_window_scaling.cc',
+        '<(DEPTH)/starboard/shared/posix/storage_write_record.cc',
+        '<(DEPTH)/starboard/shared/posix/string_compare_no_case.cc',
+        '<(DEPTH)/starboard/shared/posix/string_compare_no_case_n.cc',
+        '<(DEPTH)/starboard/shared/posix/string_compare_wide.cc',
+        '<(DEPTH)/starboard/shared/posix/string_format.cc',
+        '<(DEPTH)/starboard/shared/posix/string_format_wide.cc',
+        '<(DEPTH)/starboard/shared/posix/system_break_into_debugger.cc',
+        '<(DEPTH)/starboard/shared/posix/system_clear_last_error.cc',
+        '<(DEPTH)/starboard/shared/posix/system_get_error_string.cc',
+        '<(DEPTH)/starboard/shared/posix/system_get_last_error.cc',
+        '<(DEPTH)/starboard/shared/posix/system_get_number_of_processors.cc',
+        '<(DEPTH)/starboard/shared/posix/thread_sleep.cc',
+        '<(DEPTH)/starboard/shared/posix/time_get_monotonic_now.cc',
+        '<(DEPTH)/starboard/shared/posix/time_get_monotonic_thread_now.cc',
+        '<(DEPTH)/starboard/shared/posix/time_get_now.cc',
+        '<(DEPTH)/starboard/shared/posix/time_zone_get_current.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_broadcast.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_create.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_destroy.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_signal.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_wait.cc',
+        '<(DEPTH)/starboard/shared/pthread/condition_variable_wait_timed.cc',
+        '<(DEPTH)/starboard/shared/pthread/mutex_acquire.cc',
+        '<(DEPTH)/starboard/shared/pthread/mutex_acquire_try.cc',
+        '<(DEPTH)/starboard/shared/pthread/mutex_create.cc',
+        '<(DEPTH)/starboard/shared/pthread/mutex_destroy.cc',
+        '<(DEPTH)/starboard/shared/pthread/mutex_release.cc',
+        '<(DEPTH)/starboard/shared/pthread/once.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_create_local_key.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_create_priority.h',
+        '<(DEPTH)/starboard/shared/pthread/thread_destroy_local_key.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_detach.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_get_current.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_get_local_value.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_is_equal.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_join.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_set_local_value.cc',
+        '<(DEPTH)/starboard/shared/pthread/thread_yield.cc',
+        '<(DEPTH)/starboard/shared/pthread/types_public.h',
+        '<(DEPTH)/starboard/shared/signal/crash_signals.h',
+        '<(DEPTH)/starboard/shared/signal/crash_signals_sigaction.cc',
+        '<(DEPTH)/starboard/shared/signal/suspend_signals.cc',
+        '<(DEPTH)/starboard/shared/signal/suspend_signals.h',
+        '<(DEPTH)/starboard/shared/starboard/application.cc',
+        '<(DEPTH)/starboard/shared/starboard/application.h',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_create.cc',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_destroy.cc',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_internal.cc',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/audio_sink_is_valid.cc',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/stub_audio_sink_type.cc',
+        '<(DEPTH)/starboard/shared/starboard/audio_sink/stub_audio_sink_type.h',
+        '<(DEPTH)/starboard/shared/starboard/command_line.cc',
+        '<(DEPTH)/starboard/shared/starboard/command_line.h',
+        '<(DEPTH)/starboard/shared/starboard/directory_can_open.cc',
+        '<(DEPTH)/starboard/shared/starboard/drm/drm_close_session.cc',
+        '<(DEPTH)/starboard/shared/starboard/drm/drm_destroy_system.cc',
+        '<(DEPTH)/starboard/shared/starboard/drm/drm_generate_session_update_request.cc',
+        '<(DEPTH)/starboard/shared/starboard/drm/drm_system_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/drm/drm_update_session.cc',
+        '<(DEPTH)/starboard/shared/starboard/event_cancel.cc',
+        '<(DEPTH)/starboard/shared/starboard/event_schedule.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_mode_string_to_flags.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_storage/storage_close_record.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_storage/storage_delete_record.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_storage/storage_get_record_size.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_storage/storage_open_record.cc',
+        '<(DEPTH)/starboard/shared/starboard/file_storage/storage_read_record.cc',
+        '<(DEPTH)/starboard/shared/starboard/log_message.cc',
+        '<(DEPTH)/starboard/shared/starboard/log_raw_dump_stack.cc',
+        '<(DEPTH)/starboard/shared/starboard/log_raw_format.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/codec_util.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/codec_util.h',
+        '<(DEPTH)/starboard/shared/starboard/media/media_can_play_mime_and_key_system.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_audio_buffer_budget.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_alignment.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_allocation_unit.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_garbage_collection_duration_threshold.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_padding.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_buffer_storage_type.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_progressive_buffer_budget.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_get_video_buffer_budget.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_is_buffer_pool_allocate_on_demand.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_is_buffer_using_memory_pool.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_util.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/media_util.h',
+        '<(DEPTH)/starboard/shared/starboard/media/mime_type.cc',
+        '<(DEPTH)/starboard/shared/starboard/media/mime_type.h',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_close.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_create.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_destroy.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_get_available.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_is_sample_rate_supported.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_open.cc',
+        '<(DEPTH)/starboard/shared/starboard/microphone/microphone_read.cc',
+        '<(DEPTH)/starboard/shared/starboard/new.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/decoded_audio_internal.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/decoded_audio_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/player/input_buffer_internal.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/input_buffer_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/player/job_queue.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/job_queue.h',
+        '<(DEPTH)/starboard/shared/starboard/player/player_get_current_frame.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_get_info.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_get_info2.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_get_maximum_number_of_samples_per_write.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_internal.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/player/player_output_mode_supported.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_seek.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_seek2.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_set_volume.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_worker.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_worker.h',
+        '<(DEPTH)/starboard/shared/starboard/player/player_write_end_of_stream.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_write_sample.cc',
+        '<(DEPTH)/starboard/shared/starboard/player/player_write_sample2.cc',
+        '<(DEPTH)/starboard/shared/starboard/queue_application.cc',
+        '<(DEPTH)/starboard/shared/starboard/queue_application.h',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_cancel.cc',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_create.cc',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_destroy.cc',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_internal.h',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_start.cc',
+        '<(DEPTH)/starboard/shared/starboard/speech_recognizer/speech_recognizer_stop.cc',
+        '<(DEPTH)/starboard/shared/starboard/string_concat.cc',
+        '<(DEPTH)/starboard/shared/starboard/string_concat_wide.cc',
+        '<(DEPTH)/starboard/shared/starboard/string_copy.cc',
+        '<(DEPTH)/starboard/shared/starboard/string_copy_wide.cc',
+        '<(DEPTH)/starboard/shared/starboard/string_duplicate.cc',
+        '<(DEPTH)/starboard/shared/starboard/system_get_random_uint64.cc',
+        '<(DEPTH)/starboard/shared/starboard/system_supports_resume.cc',
+        '<(DEPTH)/starboard/shared/starboard/thread_checker.h',
+        '<(DEPTH)/starboard/shared/starboard/window_set_default_options.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_create_transformer.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_destroy_transformer.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_get_tag.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_set_authenticated_data.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_set_initialization_vector.cc',
+        '<(DEPTH)/starboard/shared/stub/cryptography_transform.cc',
+        '<(DEPTH)/starboard/shared/stub/drm_is_server_certificate_updatable.cc',
+        '<(DEPTH)/starboard/shared/stub/drm_update_server_certificate.cc',
+        '<(DEPTH)/starboard/shared/stub/image_decode.cc',
+        '<(DEPTH)/starboard/shared/stub/image_is_decode_supported.cc',
+        '<(DEPTH)/starboard/shared/stub/system_get_total_gpu_memory.cc',
+        '<(DEPTH)/starboard/shared/stub/system_get_used_gpu_memory.cc',
+        '<(DEPTH)/starboard/shared/stub/system_hide_splash_screen.cc',
+        '<(DEPTH)/starboard/shared/stub/system_request_pause.cc',
+        '<(DEPTH)/starboard/shared/stub/system_request_unpause.cc',
+      ],
+      'conditions': [
+        ['has_input_events_filter=="True"', {
+          'sources': [
+            'input_events_filter.cc',
+            'input_events_filter.h',
+          ],
+          'defines': [
+            'STARBOARD_INPUT_EVENTS_FILTER',
+          ],
+        }],
+      ],
+      'defines': [
+        # This must be defined when building Starboard, and must not when
+        # building Starboard client code.
+        'STARBOARD_IMPLEMENTATION',
+      ],
+      'dependencies': [
+        '<(DEPTH)/starboard/common/common.gyp:common',
+        '<(DEPTH)/third_party/libevent/libevent.gyp:libevent',
+        'starboard_base_symbolize',
+      ],
+    },
+  ],
+}
diff --git a/src/starboard/android/shared/starboard_platform_tests.gypi b/src/starboard/android/shared/starboard_platform_tests.gypi
new file mode 100644
index 0000000..5b40e63
--- /dev/null
+++ b/src/starboard/android/shared/starboard_platform_tests.gypi
@@ -0,0 +1,49 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'targets': [
+    {
+      'target_name': 'starboard_platform_tests',
+      'type': '<(gtest_target_type)',
+      'includes': [
+        '<(DEPTH)/starboard/shared/starboard/media/media_tests.gypi',
+      ],
+      'sources': [
+        '<(DEPTH)/starboard/common/test_main.cc',
+        '<@(media_tests_sources)',
+        'jni_env_ext_test.cc',
+      ],
+      'defines': [
+        # This allows the tests to include internal only header files.
+        'STARBOARD_IMPLEMENTATION',
+      ],
+      'dependencies': [
+        '<(DEPTH)/starboard/starboard.gyp:starboard',
+        '<(DEPTH)/testing/gmock.gyp:gmock',
+        '<(DEPTH)/testing/gtest.gyp:gtest',
+      ],
+    },
+    {
+      'target_name': 'starboard_platform_tests_deploy',
+      'type': 'none',
+      'dependencies': [
+        '<(DEPTH)/<(starboard_path)/starboard_platform_tests.gyp:starboard_platform_tests',
+      ],
+      'variables': {
+        'executable_name': 'starboard_platform_tests',
+      },
+      'includes': [ '<(DEPTH)/starboard/build/deploy.gypi' ],
+    },
+  ],
+}
diff --git a/src/starboard/android/shared/system_get_connection_type.cc b/src/starboard/android/shared/system_get_connection_type.cc
new file mode 100644
index 0000000..228726d
--- /dev/null
+++ b/src/starboard/android/shared/system_get_connection_type.cc
@@ -0,0 +1,32 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+
+using starboard::android::shared::JniEnvExt;
+
+SbSystemConnectionType SbSystemGetConnectionType() {
+  JniEnvExt* env = JniEnvExt::Get();
+  jboolean isWireless =
+      env->CallStarboardBooleanMethodOrAbort("isCurrentNetworkWireless", "()Z");
+
+  if (isWireless) {
+    return kSbSystemConnectionTypeWireless;
+  } else {
+    return kSbSystemConnectionTypeWired;
+  }
+}
diff --git a/src/starboard/android/shared/system_get_device_type.cc b/src/starboard/android/shared/system_get_device_type.cc
new file mode 100644
index 0000000..97d6a40
--- /dev/null
+++ b/src/starboard/android/shared/system_get_device_type.cc
@@ -0,0 +1,19 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+SbSystemDeviceType SbSystemGetDeviceType() {
+  return kSbSystemDeviceTypeAndroidTV;
+}
diff --git a/src/starboard/android/shared/system_get_locale_id.cc b/src/starboard/android/shared/system_get_locale_id.cc
new file mode 100644
index 0000000..2b11e0e
--- /dev/null
+++ b/src/starboard/android/shared/system_get_locale_id.cc
@@ -0,0 +1,49 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <string>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/once.h"
+#include "starboard/string.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+// A singleton class to hold a locale string
+class LocaleInfo {
+ public:
+  // The Starboard locale id
+  std::string locale_id;
+
+  LocaleInfo() {
+    JniEnvExt* env = JniEnvExt::Get();
+
+    ScopedLocalJavaRef<jstring> result(env->CallStarboardObjectMethodOrAbort(
+        "systemGetLocaleId", "()Ljava/lang/String;"));
+    locale_id = env->GetStringStandardUTFOrAbort(result.Get());
+  }
+};
+
+SB_ONCE_INITIALIZE_FUNCTION(LocaleInfo, GetLocale);
+}  // namespace
+
+const char* SbSystemGetLocaleId() {
+  return GetLocale()->locale_id.c_str();
+}
diff --git a/src/starboard/android/shared/system_get_path.cc b/src/starboard/android/shared/system_get_path.cc
new file mode 100644
index 0000000..3a2c323
--- /dev/null
+++ b/src/starboard/android/shared/system_get_path.cc
@@ -0,0 +1,109 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <linux/limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cstring>
+
+#include "starboard/android/shared/file_internal.h"
+#include "starboard/directory.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+using ::starboard::android::shared::g_app_assets_dir;
+using ::starboard::android::shared::g_app_cache_dir;
+using ::starboard::android::shared::g_app_lib_dir;
+
+bool SbSystemGetPath(SbSystemPathId path_id, char* out_path, int path_size) {
+  if (!out_path || !path_size) {
+    return false;
+  }
+
+  const int kPathSize = PATH_MAX;
+  char path[kPathSize];
+  path[0] = '\0';
+
+  switch (path_id) {
+    case kSbSystemPathContentDirectory: {
+      if (SbStringConcat(path, g_app_assets_dir, kPathSize) >= kPathSize) {
+        return false;
+      }
+      break;
+    }
+
+    case kSbSystemPathCacheDirectory: {
+      if (!SbSystemGetPath(kSbSystemPathTempDirectory, path, kPathSize)) {
+        return false;
+      }
+      if (SbStringConcat(path, "/cache", kPathSize) >= kPathSize) {
+        return false;
+      }
+
+      SbDirectoryCreate(path);
+      break;
+    }
+
+    case kSbSystemPathDebugOutputDirectory: {
+      if (!SbSystemGetPath(kSbSystemPathTempDirectory, path, kPathSize)) {
+        return false;
+      }
+      if (SbStringConcat(path, "/log", kPathSize) >= kPathSize) {
+        return false;
+      }
+
+      SbDirectoryCreate(path);
+      break;
+    }
+
+    case kSbSystemPathTempDirectory: {
+      if (SbStringCopy(path, g_app_cache_dir, kPathSize) >= kPathSize) {
+        return false;
+      }
+
+      SbDirectoryCreate(path);
+      break;
+    }
+
+    case kSbSystemPathTestOutputDirectory: {
+      return SbSystemGetPath(kSbSystemPathDebugOutputDirectory, out_path,
+                             path_size);
+    }
+
+    // We return the library directory as the "executable" since:
+    // a) Unlike the .so itself, it has a valid timestamp of the app install.
+    // b) Its parent directory is still a directory within our app package.
+    case kSbSystemPathExecutableFile: {
+      if (SbStringCopy(path, g_app_lib_dir, kPathSize) >= kPathSize) {
+        return false;
+      }
+      break;
+    }
+
+    default:
+      SB_NOTIMPLEMENTED();
+      return false;
+  }
+
+  int length = strlen(path);
+  if (length < 1 || length > path_size) {
+    return false;
+  }
+
+  SbStringCopy(out_path, path, path_size);
+  return true;
+}
diff --git a/src/starboard/android/shared/system_get_property.cc b/src/starboard/android/shared/system_get_property.cc
new file mode 100644
index 0000000..cbd49e1
--- /dev/null
+++ b/src/starboard/android/shared/system_get_property.cc
@@ -0,0 +1,137 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "sys/system_properties.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/log.h"
+#include "starboard/string.h"
+
+// We can't #include "base/stringize_macros.h" in Starboard
+#define STRINGIZE_NO_EXPANSION(x) #x
+#define STRINGIZE(x) STRINGIZE_NO_EXPANSION(x)
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+namespace {
+
+const char kFriendlyName[] = "Android";
+const char kUnknownValue[] = "unknown";
+// This is a format string template and the %s is meant to be replaced by
+// the Android release version number (e.g. "7.0" for Nougat).
+const char kPlatformNameFormat[] =
+    "Linux " STRINGIZE(ANDROID_ABI) "; Android %s";
+
+bool CopyStringAndTestIfSuccess(char* out_value,
+                                int value_length,
+                                const char* from_value) {
+  if (SbStringGetLength(from_value) + 1 > value_length)
+    return false;
+  SbStringCopy(out_value, from_value, value_length);
+  return true;
+}
+
+bool GetAndroidSystemProperty(const char* system_property_name,
+                              char* out_value,
+                              int value_length,
+                              const char* default_value) {
+  if (value_length < PROP_VALUE_MAX) {
+    return false;
+  }
+  // Note that __system_property_get returns empty string on no value
+  __system_property_get(system_property_name, out_value);
+
+  if (SbStringGetLength(out_value) == 0) {
+    SbStringCopy(out_value, default_value, value_length);
+  }
+  return true;
+}
+
+// Populate the kPlatformNameFormat with the version number and if
+// |value_length| is large enough to store the result, copy the result into
+// |out_value|.
+bool CopyAndroidPlatformName(char* out_value, int value_length) {
+  // Get the Android version number (e.g. "7.0" for Nougat).
+  const int kStringBufferSize = 256;
+  char version_string_buffer[kStringBufferSize];
+  if (!GetAndroidSystemProperty("ro.build.version.release",
+                                version_string_buffer, kStringBufferSize,
+                                kUnknownValue)) {
+    return false;
+  }
+
+  char result_string[kStringBufferSize];
+  SbStringFormatF(result_string, kStringBufferSize, kPlatformNameFormat,
+                  version_string_buffer);
+
+  return CopyStringAndTestIfSuccess(out_value, value_length, result_string);
+}
+
+}  // namespace
+
+bool SbSystemGetProperty(SbSystemPropertyId property_id,
+                         char* out_value,
+                         int value_length) {
+  if (!out_value || !value_length) {
+    return false;
+  }
+
+  switch (property_id) {
+    case kSbSystemPropertyBrandName:
+      return GetAndroidSystemProperty("ro.product.manufacturer", out_value,
+                                      value_length, kUnknownValue);
+    case kSbSystemPropertyModelName:
+      return GetAndroidSystemProperty("ro.product.model", out_value,
+                                      value_length, kUnknownValue);
+    case kSbSystemPropertyFirmwareVersion:
+      return GetAndroidSystemProperty("ro.build.id", out_value, value_length,
+                                      kUnknownValue);
+    case kSbSystemPropertyChipsetModelNumber:
+      return GetAndroidSystemProperty("ro.board.platform", out_value,
+                                      value_length, kUnknownValue);
+    case kSbSystemPropertyModelYear:
+    case kSbSystemPropertyNetworkOperatorName:
+      return false;
+
+    case kSbSystemPropertyFriendlyName:
+      return CopyStringAndTestIfSuccess(out_value, value_length, kFriendlyName);
+
+    case kSbSystemPropertyPlatformName:
+      return CopyAndroidPlatformName(out_value, value_length);
+
+    case kSbSystemPropertySpeechApiKey:
+      return false;
+    case kSbSystemPropertyUserAgentAuxField: {
+      JniEnvExt* env = JniEnvExt::Get();
+      ScopedLocalJavaRef<jstring> aux_string(
+          env->CallStarboardObjectMethodOrAbort(
+              "getUserAgentAuxField", "()Ljava/lang/String;"));
+
+      std::string utf_str = env->GetStringStandardUTFOrAbort(aux_string.Get());
+      bool success =
+          CopyStringAndTestIfSuccess(out_value, value_length, utf_str.c_str());
+      return success;
+    }
+    default:
+      SB_DLOG(WARNING) << __FUNCTION__
+                       << ": Unrecognized property: " << property_id;
+      break;
+  }
+
+  return false;
+}
diff --git a/src/starboard/android/shared/system_get_stack.cc b/src/starboard/android/shared/system_get_stack.cc
new file mode 100644
index 0000000..05434b4
--- /dev/null
+++ b/src/starboard/android/shared/system_get_stack.cc
@@ -0,0 +1,56 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <unwind.h>
+
+#include "starboard/system.h"
+#include "starboard/log.h"
+
+namespace {
+class CallbackContext {
+ public:
+  void** out_stack;
+  int stack_size;
+  int count;
+  CallbackContext(void** out_stack, int stack_size)
+      : out_stack(out_stack), stack_size(stack_size), count(0) {}
+};
+
+_Unwind_Reason_Code UnwindCallback(struct _Unwind_Context* uwc,
+                                   void* user_context) {
+  CallbackContext* callback_context =
+      static_cast<CallbackContext*>(user_context);
+  _Unwind_Ptr ip = _Unwind_GetIP(uwc);
+
+  if (ip == 0) {
+    return _URC_END_OF_STACK;
+  }
+
+  if (callback_context->count == callback_context->stack_size) {
+    return _URC_END_OF_STACK;
+  }
+
+  callback_context->out_stack[callback_context->count] =
+      reinterpret_cast<void*>(ip);
+  callback_context->count++;
+  return _URC_NO_REASON;
+}
+}  // namespace
+
+int SbSystemGetStack(void** out_stack, int stack_size) {
+  CallbackContext callback_context(out_stack, stack_size);
+
+  _Unwind_Backtrace(UnwindCallback, &callback_context);
+  return callback_context.count;
+}
diff --git a/src/starboard/android/shared/system_has_capability.cc b/src/starboard/android/shared/system_has_capability.cc
new file mode 100644
index 0000000..4d77b0d
--- /dev/null
+++ b/src/starboard/android/shared/system_has_capability.cc
@@ -0,0 +1,31 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/log.h"
+
+bool SbSystemHasCapability(SbSystemCapabilityId capability_id) {
+  switch (capability_id) {
+    case kSbSystemCapabilityReversedEnterAndBack:
+      return false;
+    case kSbSystemCapabilityCanQueryGPUMemoryStats:
+      return false;
+    case kSbSystemCapabilitySetsInputTimestamp:
+      return false;
+  }
+
+  SB_DLOG(WARNING) << "Unrecognized capability: " << capability_id;
+  return false;
+}
diff --git a/src/starboard/android/shared/system_platform_error.cc b/src/starboard/android/shared/system_platform_error.cc
new file mode 100644
index 0000000..1f66983
--- /dev/null
+++ b/src/starboard/android/shared/system_platform_error.cc
@@ -0,0 +1,92 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include <android/native_activity.h>
+#include <jni.h>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::ApplicationAndroid;
+using starboard::android::shared::JniEnvExt;
+
+struct SbSystemPlatformErrorPrivate {
+  SbSystemPlatformErrorPrivate(SbSystemPlatformErrorType type,
+                               SbSystemPlatformErrorCallback callback,
+                               void* user_data)
+      : error_obj(NULL), type(type), callback(callback), user_data(user_data) {}
+
+  jobject error_obj;  // global ref to dev.cobalt.PlatformError instance
+  SbSystemPlatformErrorType type;
+  SbSystemPlatformErrorCallback callback;
+  void* user_data;
+};
+
+namespace {
+
+enum {
+  // This must be kept in sync with Java dev.cobalt.PlatformError.ErrorType
+  kJniErrorTypeConnectionError = 0,
+};
+
+}  // namespace
+
+SbSystemPlatformError SbSystemRaisePlatformError(
+    SbSystemPlatformErrorType type,
+    SbSystemPlatformErrorCallback callback,
+    void* user_data) {
+  jint jni_error_type;
+  switch (type) {
+    case kSbSystemPlatformErrorTypeConnectionError:
+      jni_error_type = kJniErrorTypeConnectionError;
+      break;
+    default:
+      SB_NOTREACHED();
+      return kSbSystemPlatformErrorInvalid;
+  }
+
+  SbSystemPlatformError error_handle =
+      new SbSystemPlatformErrorPrivate(type, callback, user_data);
+
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject error_obj = env->CallStarboardObjectMethodOrAbort(
+      "raisePlatformError", "(IJ)Ldev/cobalt/coat/PlatformError;",
+      jni_error_type, reinterpret_cast<jlong>(error_handle));
+  error_handle->error_obj = env->NewGlobalRef(error_obj);
+
+  return error_handle;
+}
+
+void SbSystemClearPlatformError(SbSystemPlatformError handle) {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallObjectMethodOrAbort(handle->error_obj, "clear", "()V");
+}
+
+extern "C" SB_EXPORT_PLATFORM
+void Java_dev_cobalt_coat_PlatformError_nativeOnCleared(
+    JNIEnv* env, jobject unused_this, jint jni_response, jlong jni_data) {
+  SB_UNREFERENCED_PARAMETER(unused_this);
+  SbSystemPlatformError error_handle =
+      reinterpret_cast<SbSystemPlatformError>(jni_data);
+  env->DeleteGlobalRef(error_handle->error_obj);
+  if (error_handle->callback) {
+    SbSystemPlatformErrorResponse error_response =
+        jni_response < 0 ? kSbSystemPlatformErrorResponseNegative :
+        jni_response > 0 ? kSbSystemPlatformErrorResponsePositive :
+        kSbSystemPlatformErrorResponseCancel;
+    error_handle->callback(error_response, error_handle->user_data);
+  }
+}
diff --git a/src/starboard/android/shared/system_request_stop.cc b/src/starboard/android/shared/system_request_stop.cc
new file mode 100644
index 0000000..85aab60
--- /dev/null
+++ b/src/starboard/android/shared/system_request_stop.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+void SbSystemRequestStop(int error_level) {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallStarboardVoidMethodOrAbort("requestStop", "(I)V", error_level);
+}
diff --git a/src/starboard/android/shared/system_request_suspend.cc b/src/starboard/android/shared/system_request_suspend.cc
new file mode 100644
index 0000000..c8f53c4
--- /dev/null
+++ b/src/starboard/android/shared/system_request_suspend.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/system.h"
+
+#include "starboard/android/shared/jni_env_ext.h"
+
+using starboard::android::shared::JniEnvExt;
+
+void SbSystemRequestSuspend() {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallStarboardVoidMethodOrAbort("requestSuspend", "()V");
+}
diff --git a/src/starboard/android/shared/thread_create.cc b/src/starboard/android/shared/thread_create.cc
new file mode 100644
index 0000000..8aee6dd
--- /dev/null
+++ b/src/starboard/android/shared/thread_create.cc
@@ -0,0 +1,149 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/thread.h"
+
+#include <pthread.h>
+#include <sched.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/log.h"
+#include "starboard/shared/pthread/is_success.h"
+#include "starboard/shared/pthread/thread_create_priority.h"
+#include "starboard/string.h"
+
+namespace starboard {
+namespace shared {
+namespace pthread {
+
+#if !SB_HAS(THREAD_PRIORITY_SUPPORT)
+// Default implementation without thread priority support
+void ThreadSetPriority(SbThreadPriority /* priority */) {}
+#endif
+
+void PreThreadRun() {}
+
+void PostThreadRun() {
+  // TODO: Currently OnThreadFinish() is specific to the android-forked version
+  // of this file, thread_create.cc.  This functionality could be implemented
+  // though similar to ThreadSetPriority(), such that we allow platforms to
+  // override the OnThreadStart()/OnThreadFinish() logic, if they want, and then
+  // we don't have to fork this file.  The only reason that is not done
+  // currently is because of bad timing with respect to Starboard interface
+  // changes.
+  android::shared::JniEnvExt::OnThreadShutdown();
+}
+
+}  // namespace pthread
+}  // namespace shared
+}  // namespace starboard
+
+namespace {
+
+struct ThreadParams {
+  SbThreadAffinity affinity;
+  SbThreadEntryPoint entry_point;
+  char name[128];
+  void* context;
+  SbThreadPriority priority;
+};
+
+void* ThreadFunc(void* context) {
+  ThreadParams* thread_params = static_cast<ThreadParams*>(context);
+  SbThreadEntryPoint entry_point = thread_params->entry_point;
+  void* real_context = thread_params->context;
+  SbThreadAffinity affinity = thread_params->affinity;
+  if (thread_params->name[0] != '\0') {
+    SbThreadSetName(thread_params->name);
+  }
+
+  starboard::shared::pthread::ThreadSetPriority(thread_params->priority);
+
+  delete thread_params;
+
+  if (SbThreadIsValidAffinity(affinity)) {
+    cpu_set_t cpu_set;
+    CPU_ZERO(&cpu_set);
+    CPU_SET(affinity, &cpu_set);
+    sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
+  }
+
+  starboard::shared::pthread::PreThreadRun();
+
+  void* return_value = entry_point(real_context);
+
+  starboard::shared::pthread::PostThreadRun();
+
+  return return_value;
+}
+
+}  // namespace
+
+SbThread SbThreadCreate(int64_t stack_size,
+                        SbThreadPriority priority,
+                        SbThreadAffinity affinity,
+                        bool joinable,
+                        const char* name,
+                        SbThreadEntryPoint entry_point,
+                        void* context) {
+  if (stack_size < 0 || !entry_point) {
+    return kSbThreadInvalid;
+  }
+
+#if defined(ADDRESS_SANITIZER)
+  // Set a big thread stack size when in ADDRESS_SANITIZER mode.
+  // This eliminates buffer overflows for deeply nested callstacks.
+  if (stack_size == 0) {
+    stack_size = 4096 * 1024;  // 4MB
+  }
+#endif
+
+  pthread_attr_t attributes;
+  int result = pthread_attr_init(&attributes);
+  if (!IsSuccess(result)) {
+    return kSbThreadInvalid;
+  }
+
+  pthread_attr_setdetachstate(
+      &attributes,
+      (joinable ? PTHREAD_CREATE_JOINABLE : PTHREAD_CREATE_DETACHED));
+  if (stack_size > 0) {
+    pthread_attr_setstacksize(&attributes, stack_size);
+  }
+
+  ThreadParams* params = new ThreadParams();
+  params->affinity = affinity;
+  params->entry_point = entry_point;
+  params->context = context;
+
+  if (name) {
+    SbStringCopy(params->name, name, SB_ARRAY_SIZE_INT(params->name));
+  } else {
+    params->name[0] = '\0';
+  }
+
+  params->priority = priority;
+
+  SbThread thread = kSbThreadInvalid;
+  result = pthread_create(&thread, &attributes, ThreadFunc, params);
+
+  pthread_attr_destroy(&attributes);
+  if (IsSuccess(result)) {
+    return thread;
+  }
+
+  return kSbThreadInvalid;
+}
diff --git a/src/starboard/android/shared/thread_create_priority.cc b/src/starboard/android/shared/thread_create_priority.cc
new file mode 100644
index 0000000..5247c58
--- /dev/null
+++ b/src/starboard/android/shared/thread_create_priority.cc
@@ -0,0 +1,68 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/pthread/thread_create_priority.h"
+
+#include <sched.h>
+#include <sys/resource.h>
+
+#include "starboard/log.h"
+
+namespace starboard {
+namespace shared {
+namespace pthread {
+
+#if SB_HAS(THREAD_PRIORITY_SUPPORT)
+
+void SetNiceValue(int nice) {
+  int result = setpriority(PRIO_PROCESS, 0, nice);
+  if (result != 0) {
+    SB_NOTREACHED();
+  }
+}
+
+void ThreadSetPriority(SbThreadPriority priority) {
+  // Nice value settings are selected from looking at:
+  //   https://android.googlesource.com/platform/frameworks/native/+/jb-dev/include/utils/ThreadDefs.h#35
+  switch (priority) {
+    case kSbThreadPriorityLowest:
+      SetNiceValue(19);
+      break;
+    case kSbThreadPriorityLow:
+      SetNiceValue(10);
+      break;
+    case kSbThreadNoPriority:
+    case kSbThreadPriorityNormal:
+      SetNiceValue(0);
+      break;
+    case kSbThreadPriorityHigh:
+      SetNiceValue(-8);
+      break;
+    case kSbThreadPriorityHighest:
+      SetNiceValue(-16);
+      break;
+    case kSbThreadPriorityRealTime:
+      SetNiceValue(-19);
+      break;
+    default:
+      SB_NOTREACHED();
+      break;
+  }
+}
+
+#endif  // SB_HAS(THREAD_PRIORITY_SUPPORT)
+
+}  // namespace pthread
+}  // namespace shared
+}  // namespace starboard
diff --git a/src/starboard/android/shared/thread_get_name.cc b/src/starboard/android/shared/thread_get_name.cc
new file mode 100644
index 0000000..3adbf9a
--- /dev/null
+++ b/src/starboard/android/shared/thread_get_name.cc
@@ -0,0 +1,20 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/thread.h"
+#include <sys/prctl.h>
+
+void SbThreadGetName(char* buffer, int buffer_size) {
+  prctl(PR_GET_NAME, buffer, 0L, 0L, 0L);
+}
diff --git a/src/starboard/android/shared/thread_types_public.h b/src/starboard/android/shared/thread_types_public.h
new file mode 100644
index 0000000..266f513
--- /dev/null
+++ b/src/starboard/android/shared/thread_types_public.h
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Includes threading primitive types and initializers.
+
+#ifndef STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
+#define STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
+
+#include "starboard/shared/pthread/types_public.h"
+
+#endif  // STARBOARD_ANDROID_SHARED_THREAD_TYPES_PUBLIC_H_
diff --git a/src/starboard/android/shared/time_zone_get_dst_name.cc b/src/starboard/android/shared/time_zone_get_dst_name.cc
new file mode 100644
index 0000000..fbdeb86
--- /dev/null
+++ b/src/starboard/android/shared/time_zone_get_dst_name.cc
@@ -0,0 +1,33 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/time_zone.h"
+
+#include <time.h>
+
+#if SB_API_VERSION < 6
+const char* SbTimeZoneGetDstName() {
+  // Note tzset() is called in ApplicationAndroid::Initialize()
+
+  // Android's bionic seems not to set tzname[1] when selecting GMT
+  // because timezone is not otherwise available.
+  // But glibc returns "GMT" in both tzname[0] and tzname[1] when
+  // GMT is selected.
+  if (tzname[1][0] == '\0') {
+    return tzname[0];
+  } else {
+    return tzname[1];
+  }
+}
+#endif  // SB_API_VERSION < 6
diff --git a/src/starboard/android/shared/time_zone_get_name.cc b/src/starboard/android/shared/time_zone_get_name.cc
new file mode 100644
index 0000000..b2b6c41
--- /dev/null
+++ b/src/starboard/android/shared/time_zone_get_name.cc
@@ -0,0 +1,23 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/time_zone.h"
+
+#include <time.h>
+
+const char* SbTimeZoneGetName() {
+  // Note tzset() is called in ApplicationAndroid::Initialize()
+
+  return tzname[0];
+}
diff --git a/src/starboard/android/shared/trace_util.h b/src/starboard/android/shared/trace_util.h
new file mode 100644
index 0000000..96a8ac2
--- /dev/null
+++ b/src/starboard/android/shared/trace_util.h
@@ -0,0 +1,46 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
+#define STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// A simple scoped wrapper of |android.os.Trace|.
+struct ScopedTrace {
+  explicit ScopedTrace(const char* section_name) {
+    JniEnvExt* env = JniEnvExt::Get();
+    ScopedLocalJavaRef<jstring> j_section_name(
+        env->NewStringStandardUTFOrAbort(section_name));
+    env->CallStaticVoidMethodOrAbort("android/os/Trace", "beginSection",
+                                     "(Ljava/lang/String;)V",
+                                     j_section_name.Get());
+  }
+
+  ~ScopedTrace() {
+    JniEnvExt* env = JniEnvExt::Get();
+    env->CallStaticVoidMethodOrAbort("android/os/Trace", "endSection", "()V");
+  }
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_TRACE_UTIL_H_
diff --git a/src/starboard/android/shared/video_decoder.cc b/src/starboard/android/shared/video_decoder.cc
new file mode 100644
index 0000000..a27d637
--- /dev/null
+++ b/src/starboard/android/shared/video_decoder.cc
@@ -0,0 +1,522 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_decoder.h"
+
+#include <jni.h>
+
+#include <cmath>
+#include <functional>
+
+#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/decode_target_create.h"
+#include "starboard/android/shared/decode_target_internal.h"
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+#include "starboard/android/shared/video_window.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/configuration.h"
+#include "starboard/decode_target.h"
+#include "starboard/drm.h"
+#include "starboard/memory.h"
+#include "starboard/shared/starboard/player/filter/video_frame_internal.h"
+#include "starboard/string.h"
+#include "starboard/thread.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+using ::starboard::shared::starboard::player::filter::VideoFrame;
+using std::placeholders::_1;
+using std::placeholders::_2;
+
+class VideoFrameImpl : public VideoFrame {
+ public:
+  VideoFrameImpl(const DequeueOutputResult& dequeue_output_result,
+                 MediaCodecBridge* media_codec_bridge)
+      : VideoFrame(dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM
+                       ? kMediaTimeEndOfStream
+                       : dequeue_output_result.presentation_time_microseconds),
+        dequeue_output_result_(dequeue_output_result),
+        media_codec_bridge_(media_codec_bridge),
+        released_(false) {
+    SB_DCHECK(media_codec_bridge_);
+  }
+
+  ~VideoFrameImpl() {
+    if (!released_) {
+      media_codec_bridge_->ReleaseOutputBuffer(dequeue_output_result_.index,
+                                               false);
+      if (is_end_of_stream()) {
+        media_codec_bridge_->Flush();
+      }
+    }
+  }
+
+  void Draw(int64_t release_time_in_nanoseconds) {
+    SB_DCHECK(!released_);
+    SB_DCHECK(!is_end_of_stream());
+    released_ = true;
+    media_codec_bridge_->ReleaseOutputBufferAtTimestamp(
+        dequeue_output_result_.index, release_time_in_nanoseconds);
+  }
+
+ private:
+  DequeueOutputResult dequeue_output_result_;
+  MediaCodecBridge* media_codec_bridge_;
+  volatile bool released_;
+};
+
+const SbTime kInitialPrerollTimeout = 250 * kSbTimeMillisecond;
+
+const int kInitialPrerollFrameCount = 8;
+const int kNonInitialPrerollFrameCount = 1;
+
+const int kMaxPendingWorkSize = 128;
+
+// Convenience HDR mastering metadata.
+const SbMediaMasteringMetadata kEmptyMasteringMetadata = {};
+
+// Determine if two |SbMediaMasteringMetadata|s are equal.
+bool Equal(const SbMediaMasteringMetadata& lhs,
+           const SbMediaMasteringMetadata& rhs) {
+  return SbMemoryCompare(&lhs, &rhs, sizeof(SbMediaMasteringMetadata)) == 0;
+}
+
+// Determine if two |SbMediaColorMetadata|s are equal.
+bool Equal(const SbMediaColorMetadata& lhs, const SbMediaColorMetadata& rhs) {
+  return SbMemoryCompare(&lhs, &rhs, sizeof(SbMediaMasteringMetadata)) == 0;
+}
+
+// TODO: For whatever reason, Cobalt will always pass us this us for
+// color metadata, regardless of whether HDR is on or not.  Find out if this
+// is intentional or not.  It would make more sense if it were NULL.
+// Determine if |color_metadata| is "empty", or "null".
+bool IsIdentity(const SbMediaColorMetadata& color_metadata) {
+  return color_metadata.primaries == kSbMediaPrimaryIdBt709 &&
+         color_metadata.transfer == kSbMediaTransferIdBt709 &&
+         color_metadata.matrix == kSbMediaMatrixIdBt709 &&
+         color_metadata.range == kSbMediaRangeIdLimited &&
+         Equal(color_metadata.mastering_metadata, kEmptyMasteringMetadata);
+}
+
+}  // namespace
+
+class VideoDecoder::Sink : public VideoDecoder::VideoRendererSink {
+ public:
+  bool Render() {
+    SB_DCHECK(render_cb_);
+
+    rendered_ = false;
+    render_cb_(std::bind(&Sink::DrawFrame, this, _1, _2));
+
+    return rendered_;
+  }
+
+ private:
+  void SetRenderCB(RenderCB render_cb) override {
+    SB_DCHECK(!render_cb_);
+    SB_DCHECK(render_cb);
+
+    render_cb_ = render_cb;
+  }
+
+  void SetBounds(int z_index, int x, int y, int width, int height) override {
+    SB_UNREFERENCED_PARAMETER(z_index);
+    SB_UNREFERENCED_PARAMETER(x);
+    SB_UNREFERENCED_PARAMETER(y);
+    SB_UNREFERENCED_PARAMETER(width);
+    SB_UNREFERENCED_PARAMETER(height);
+  }
+
+  DrawFrameStatus DrawFrame(const scoped_refptr<VideoFrame>& frame,
+                            int64_t release_time_in_nanoseconds) {
+    rendered_ = true;
+    static_cast<VideoFrameImpl*>(frame.get())
+        ->Draw(release_time_in_nanoseconds);
+
+    return kReleased;
+  }
+
+  RenderCB render_cb_;
+  bool rendered_;
+};
+
+VideoDecoder::VideoDecoder(SbMediaVideoCodec video_codec,
+                           SbDrmSystem drm_system,
+                           SbPlayerOutputMode output_mode,
+                           SbDecodeTargetGraphicsContextProvider*
+                               decode_target_graphics_context_provider)
+    : video_codec_(video_codec),
+      drm_system_(static_cast<DrmSystem*>(drm_system)),
+      output_mode_(output_mode),
+      decode_target_graphics_context_provider_(
+          decode_target_graphics_context_provider),
+      decode_target_(kSbDecodeTargetInvalid),
+      frame_width_(0),
+      frame_height_(0),
+      first_buffer_received_(false) {
+  if (!InitializeCodec()) {
+    SB_LOG(ERROR) << "Failed to initialize video decoder.";
+    TeardownCodec();
+  }
+}
+
+VideoDecoder::~VideoDecoder() {
+  TeardownCodec();
+  ClearVideoWindow();
+}
+
+scoped_refptr<VideoDecoder::VideoRendererSink> VideoDecoder::GetSink() {
+  if (sink_ == NULL) {
+    sink_ = new Sink;
+  }
+  return sink_;
+}
+
+void VideoDecoder::Initialize(const DecoderStatusCB& decoder_status_cb,
+                              const ErrorCB& error_cb) {
+  SB_DCHECK(media_decoder_);
+
+  SB_DCHECK(decoder_status_cb);
+  SB_DCHECK(!decoder_status_cb_);
+  SB_DCHECK(error_cb);
+  SB_DCHECK(!error_cb_);
+
+  decoder_status_cb_ = decoder_status_cb;
+  error_cb_ = error_cb;
+
+  media_decoder_->Initialize(error_cb_);
+}
+
+size_t VideoDecoder::GetPrerollFrameCount() const {
+  if (first_buffer_received_ && first_buffer_timestamp_ != 0) {
+    return kNonInitialPrerollFrameCount;
+  }
+  return kInitialPrerollFrameCount;
+}
+
+SbTime VideoDecoder::GetPrerollTimeout() const {
+  if (first_buffer_received_ && first_buffer_timestamp_ != 0) {
+    return kSbTimeMax;
+  }
+  return kInitialPrerollTimeout;
+}
+
+void VideoDecoder::WriteInputBuffer(
+    const scoped_refptr<InputBuffer>& input_buffer) {
+  SB_DCHECK(input_buffer);
+  SB_DCHECK(decoder_status_cb_);
+
+  if (!first_buffer_received_) {
+    first_buffer_received_ = true;
+    first_buffer_timestamp_ = input_buffer->timestamp();
+
+    // If color metadata is present and is not an identity mapping, then
+    // teardown the codec so it can be reinitalized with the new metadata.
+    auto* color_metadata = input_buffer->video_sample_info()->color_metadata;
+    if (color_metadata && !IsIdentity(*color_metadata)) {
+      SB_DCHECK(!color_metadata_) << "Unexpected residual color metadata.";
+      SB_LOG(INFO) << "Reinitializing codec with HDR color metadata.";
+      TeardownCodec();
+      color_metadata_ = *color_metadata;
+    }
+
+    // Re-initialize the codec now if it was torn down either in |Reset| or
+    // because we need to change the color metadata.
+    if (media_decoder_ == NULL) {
+      if (!InitializeCodec()) {
+        // TODO: Communicate this failure to our clients somehow.
+        SB_LOG(ERROR) << "Failed to reinitialize codec.";
+      }
+    }
+  }
+
+  media_decoder_->WriteInputBuffer(input_buffer);
+  if (number_of_frames_being_decoded_.increment() < kMaxPendingWorkSize) {
+    decoder_status_cb_(kNeedMoreInput, NULL);
+  }
+}
+
+void VideoDecoder::WriteEndOfStream() {
+  SB_DCHECK(decoder_status_cb_);
+
+  if (!first_buffer_received_) {
+    first_buffer_received_ = true;
+    first_buffer_timestamp_ = 0;
+  }
+
+  media_decoder_->WriteEndOfStream();
+}
+
+void VideoDecoder::Reset() {
+  TeardownCodec();
+  number_of_frames_being_decoded_.store(0);
+  first_buffer_received_ = false;
+}
+
+bool VideoDecoder::InitializeCodec() {
+  // Setup the output surface object.  If we are in punch-out mode, target
+  // the passed in Android video surface.  If we are in decode-to-texture
+  // mode, create a surface from a new texture target and use that as the
+  // output surface.
+  jobject j_output_surface = NULL;
+  switch (output_mode_) {
+    case kSbPlayerOutputModePunchOut: {
+      j_output_surface = GetVideoSurface();
+    } break;
+    case kSbPlayerOutputModeDecodeToTexture: {
+      // A width and height of (0, 0) is provided here because Android doesn't
+      // actually allocate any memory into the texture at this time.  That is
+      // done behind the scenes, the acquired texture is not actually backed
+      // by texture data until updateTexImage() is called on it.
+      SbDecodeTarget decode_target =
+          DecodeTargetCreate(decode_target_graphics_context_provider_,
+                             kSbDecodeTargetFormat1PlaneRGBA, 0, 0);
+      if (!SbDecodeTargetIsValid(decode_target)) {
+        SB_LOG(ERROR) << "Could not acquire a decode target from provider.";
+        return false;
+      }
+      j_output_surface = decode_target->data->surface;
+
+      starboard::ScopedLock lock(decode_target_mutex_);
+      decode_target_ = decode_target;
+    } break;
+    case kSbPlayerOutputModeInvalid: {
+      SB_NOTREACHED();
+    } break;
+  }
+  if (!j_output_surface) {
+    SB_LOG(ERROR) << "Video surface does not exist.";
+    return false;
+  }
+
+  ANativeWindow* video_window = GetVideoWindow();
+  if (!video_window) {
+    SB_LOG(ERROR)
+        << "Can't initialize the codec since we don't have a video window.";
+    return false;
+  }
+  int width = ANativeWindow_getWidth(video_window);
+  int height = ANativeWindow_getHeight(video_window);
+
+  jobject j_media_crypto = drm_system_ ? drm_system_->GetMediaCrypto() : NULL;
+  SB_DCHECK(!drm_system_ || j_media_crypto);
+  media_decoder_.reset(new MediaDecoder(
+      this, video_codec_, width, height, j_output_surface, drm_system_,
+      color_metadata_ ? &*color_metadata_ : nullptr));
+  if (media_decoder_->is_valid()) {
+    if (error_cb_) {
+      media_decoder_->Initialize(error_cb_);
+    }
+    return true;
+  }
+  media_decoder_.reset();
+  return false;
+}
+
+void VideoDecoder::TeardownCodec() {
+  media_decoder_.reset();
+  color_metadata_ = starboard::nullopt;
+
+  starboard::ScopedLock lock(decode_target_mutex_);
+  if (SbDecodeTargetIsValid(decode_target_)) {
+    SbDecodeTargetReleaseInGlesContext(decode_target_graphics_context_provider_,
+                                       decode_target_);
+    decode_target_ = kSbDecodeTargetInvalid;
+  }
+}
+
+void VideoDecoder::ProcessOutputBuffer(
+    MediaCodecBridge* media_codec_bridge,
+    const DequeueOutputResult& dequeue_output_result) {
+  SB_DCHECK(decoder_status_cb_);
+  SB_DCHECK(dequeue_output_result.index >= 0);
+
+  number_of_frames_being_decoded_.decrement();
+  bool is_end_of_stream =
+      dequeue_output_result.flags & BUFFER_FLAG_END_OF_STREAM;
+  decoder_status_cb_(
+      is_end_of_stream ? kBufferFull : kNeedMoreInput,
+      new VideoFrameImpl(dequeue_output_result, media_codec_bridge));
+}
+
+void VideoDecoder::RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) {
+  SB_DCHECK(media_codec_bridge);
+  SB_DLOG(INFO) << "Output format changed, trying to dequeue again.";
+  // Record the latest width/height of the decoded input.
+  SurfaceDimensions output_dimensions =
+      media_codec_bridge->GetOutputDimensions();
+  frame_width_ = output_dimensions.width;
+  frame_height_ = output_dimensions.height;
+}
+
+bool VideoDecoder::Tick(MediaCodecBridge* media_codec_bridge) {
+  return sink_->Render();
+}
+
+void VideoDecoder::OnFlushing() {
+  decoder_status_cb_(kReleaseAllFrames, NULL);
+}
+
+namespace {
+void updateTexImage(jobject surface_texture) {
+  JniEnvExt* env = JniEnvExt::Get();
+  env->CallVoidMethodOrAbort(surface_texture, "updateTexImage", "()V");
+}
+
+void getTransformMatrix(jobject surface_texture, float* matrix4x4) {
+  JniEnvExt* env = JniEnvExt::Get();
+
+  jfloatArray java_array = env->NewFloatArray(16);
+  SB_DCHECK(java_array);
+
+  env->CallVoidMethodOrAbort(surface_texture, "getTransformMatrix", "([F)V",
+                             java_array);
+
+  jfloat* array_values = env->GetFloatArrayElements(java_array, 0);
+  SbMemoryCopy(matrix4x4, array_values, sizeof(float) * 16);
+
+  env->DeleteLocalRef(java_array);
+}
+
+// Rounds the float to the nearest integer, and also does a DCHECK to make sure
+// that the input float was already near an integer value.
+int RoundToNearInteger(float x) {
+  int rounded = static_cast<int>(x + 0.5f);
+  return rounded;
+}
+
+// Converts a 4x4 matrix representing the texture coordinate transform into
+// an equivalent rectangle representing the region within the texture where
+// the pixel data is valid.  Note that the width and height of this region may
+// be negative to indicate that that axis should be flipped.
+void SetDecodeTargetContentRegionFromMatrix(
+    SbDecodeTargetInfoContentRegion* content_region,
+    int width,
+    int height,
+    const float* matrix4x4) {
+  // Ensure that this matrix contains no rotations or shears.  In other words,
+  // make sure that we can convert it to a decode target content region without
+  // losing any information.
+  SB_DCHECK(matrix4x4[1] == 0.0f);
+  SB_DCHECK(matrix4x4[2] == 0.0f);
+  SB_DCHECK(matrix4x4[3] == 0.0f);
+
+  SB_DCHECK(matrix4x4[4] == 0.0f);
+  SB_DCHECK(matrix4x4[6] == 0.0f);
+  SB_DCHECK(matrix4x4[7] == 0.0f);
+
+  SB_DCHECK(matrix4x4[8] == 0.0f);
+  SB_DCHECK(matrix4x4[9] == 0.0f);
+  SB_DCHECK(matrix4x4[10] == 1.0f);
+  SB_DCHECK(matrix4x4[11] == 0.0f);
+
+  SB_DCHECK(matrix4x4[14] == 0.0f);
+  SB_DCHECK(matrix4x4[15] == 1.0f);
+
+  float origin_x = matrix4x4[12];
+  float origin_y = matrix4x4[13];
+
+  float extent_x = matrix4x4[0] + matrix4x4[12];
+  float extent_y = matrix4x4[5] + matrix4x4[13];
+
+  SB_DCHECK(origin_y >= 0.0f);
+  SB_DCHECK(origin_y <= 1.0f);
+  SB_DCHECK(origin_x >= 0.0f);
+  SB_DCHECK(origin_x <= 1.0f);
+  SB_DCHECK(extent_x >= 0.0f);
+  SB_DCHECK(extent_x <= 1.0f);
+  SB_DCHECK(extent_y >= 0.0f);
+  SB_DCHECK(extent_y <= 1.0f);
+
+  // Flip the y-axis to match ContentRegion's coordinate system.
+  origin_y = 1.0f - origin_y;
+  extent_y = 1.0f - extent_y;
+
+  content_region->left = RoundToNearInteger(origin_x * width);
+  content_region->right = RoundToNearInteger(extent_x * width);
+
+  // Note that in GL coordinates, the origin is the bottom and the extent
+  // is the top.
+  content_region->top = RoundToNearInteger(extent_y * height);
+  content_region->bottom = RoundToNearInteger(origin_y * height);
+}
+}  // namespace
+
+// When in decode-to-texture mode, this returns the current decoded video frame.
+SbDecodeTarget VideoDecoder::GetCurrentDecodeTarget() {
+  SB_DCHECK(output_mode_ == kSbPlayerOutputModeDecodeToTexture);
+  // We must take a lock here since this function can be called from a separate
+  // thread.
+  starboard::ScopedLock lock(decode_target_mutex_);
+
+  if (SbDecodeTargetIsValid(decode_target_)) {
+    updateTexImage(decode_target_->data->surface_texture);
+
+    float matrix4x4[16];
+    getTransformMatrix(decode_target_->data->surface_texture, matrix4x4);
+    SetDecodeTargetContentRegionFromMatrix(
+        &decode_target_->data->info.planes[0].content_region, frame_width_,
+        frame_height_, matrix4x4);
+
+    // Take this opportunity to update the decode target's width and height.
+    decode_target_->data->info.planes[0].width = frame_width_;
+    decode_target_->data->info.planes[0].height = frame_height_;
+    decode_target_->data->info.width = frame_width_;
+    decode_target_->data->info.height = frame_height_;
+
+    SbDecodeTarget out_decode_target = new SbDecodeTargetPrivate;
+    out_decode_target->data = decode_target_->data;
+
+    return out_decode_target;
+  } else {
+    return kSbDecodeTargetInvalid;
+  }
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+namespace starboard {
+namespace shared {
+namespace starboard {
+namespace player {
+namespace filter {
+
+// static
+bool VideoDecoder::OutputModeSupported(SbPlayerOutputMode output_mode,
+                                       SbMediaVideoCodec codec,
+                                       SbDrmSystem drm_system) {
+  if (output_mode == kSbPlayerOutputModePunchOut) {
+    return true;
+  }
+
+  if (output_mode == kSbPlayerOutputModeDecodeToTexture) {
+    return !SbDrmSystemIsValid(drm_system);
+  }
+
+  return false;
+}
+
+}  // namespace filter
+}  // namespace player
+}  // namespace starboard
+}  // namespace shared
+}  // namespace starboard
diff --git a/src/starboard/android/shared/video_decoder.h b/src/starboard/android/shared/video_decoder.h
new file mode 100644
index 0000000..0a4bd13
--- /dev/null
+++ b/src/starboard/android/shared/video_decoder.h
@@ -0,0 +1,124 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
+
+#include <deque>
+
+#include "starboard/android/shared/drm_system.h"
+#include "starboard/android/shared/media_codec_bridge.h"
+#include "starboard/android/shared/media_decoder.h"
+#include "starboard/atomic.h"
+#include "starboard/common/optional.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/decode_target.h"
+#include "starboard/media.h"
+#include "starboard/player.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_sink.h"
+#include "starboard/shared/starboard/player/input_buffer_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoDecoder
+    : public ::starboard::shared::starboard::player::filter::VideoDecoder,
+      private MediaDecoder::Host {
+ public:
+  typedef ::starboard::shared::starboard::player::filter::VideoRendererSink
+      VideoRendererSink;
+
+  class Sink;
+
+  VideoDecoder(SbMediaVideoCodec video_codec,
+               SbDrmSystem drm_system,
+               SbPlayerOutputMode output_mode,
+               SbDecodeTargetGraphicsContextProvider*
+                   decode_target_graphics_context_provider);
+  ~VideoDecoder() override;
+
+  scoped_refptr<VideoRendererSink> GetSink();
+
+  void Initialize(const DecoderStatusCB& decoder_status_cb,
+                  const ErrorCB& error_cb) override;
+  size_t GetPrerollFrameCount() const override;
+  SbTime GetPrerollTimeout() const override;
+  size_t GetMaxNumberOfCachedFrames() const override { return 12; }
+
+  void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer)
+      override;
+  void WriteEndOfStream() override;
+  void Reset() override;
+  SbDecodeTarget GetCurrentDecodeTarget() override;
+
+  bool is_valid() const { return media_decoder_ != NULL; }
+
+ private:
+  // Attempt to initialize the codec.  Returns whether initialization was
+  // successful.
+  bool InitializeCodec();
+  void TeardownCodec();
+
+  void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
+                           const DequeueOutputResult& output) override;
+  void RefreshOutputFormat(MediaCodecBridge* media_codec_bridge) override;
+  bool Tick(MediaCodecBridge* media_codec_bridge) override;
+  void OnFlushing() override;
+
+  // These variables will be initialized inside ctor or Initialize() and will
+  // not be changed during the life time of this class.
+  const SbMediaVideoCodec video_codec_;
+  DecoderStatusCB decoder_status_cb_;
+  ErrorCB error_cb_;
+  DrmSystem* drm_system_;
+
+  SbPlayerOutputMode output_mode_;
+
+  SbDecodeTargetGraphicsContextProvider*
+      decode_target_graphics_context_provider_;
+
+  // If decode-to-texture is enabled, then we store the decode target texture
+  // inside of this |decode_target_| member.
+  SbDecodeTarget decode_target_;
+
+  // Since GetCurrentDecodeTarget() needs to be called from an arbitrary thread
+  // to obtain the current decode target (which ultimately ends up being a
+  // copy of |decode_target_|), we need to safe-guard access to |decode_target_|
+  // and we do so through this mutex.
+  starboard::Mutex decode_target_mutex_;
+
+  // The width and height of the latest decoded frame.
+  int32_t frame_width_;
+  int32_t frame_height_;
+
+  // The last enqueued |SbMediaColorMetadata|.
+  optional<SbMediaColorMetadata> color_metadata_;
+
+  scoped_ptr<MediaDecoder> media_decoder_;
+
+  atomic_int32_t number_of_frames_being_decoded_;
+  scoped_refptr<Sink> sink_;
+
+  bool first_buffer_received_;
+  volatile SbTime first_buffer_timestamp_;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
diff --git a/src/starboard/android/shared/video_frame_release_time_adjuster.cc b/src/starboard/android/shared/video_frame_release_time_adjuster.cc
new file mode 100644
index 0000000..30f4b07
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_release_time_adjuster.cc
@@ -0,0 +1,170 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_frame_release_time_adjuster.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper() {
+  this(DISPLAY_REFRESH_RATE_UNKNOWN);
+}
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper(Context context) {
+  this(getDefaultDisplayRefreshRate(context));
+}
+
+VideoFrameReleaseTimeHelper::VideoFrameReleaseTimeHelper(
+    double defaultDisplayRefreshRate) {
+  useDefaultDisplayVsync =
+      defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN;
+  if (useDefaultDisplayVsync) {
+    vsyncSampler = VSyncSampler.getInstance();
+    vsyncDurationNs = (long)(NANOS_PER_SECOND / defaultDisplayRefreshRate);
+    vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+  } else {
+    vsyncSampler = null;
+    vsyncDurationNs = -1;  // Value unused.
+    vsyncOffsetNs = -1;    // Value unused.
+  }
+}
+
+void VideoFrameReleaseTimeHelper::enable() {
+  haveSync = false;
+  if (useDefaultDisplayVsync) {
+    vsyncSampler.addObserver();
+  }
+}
+
+void VideoFrameReleaseTimeHelper::disable() {
+  if (useDefaultDisplayVsync) {
+    vsyncSampler.removeObserver();
+  }
+}
+
+long VideoFrameReleaseTimeHelper::adjustReleaseTime(
+    long framePresentationTimeUs,
+    long unadjustedReleaseTimeNs) {
+  long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+  // Until we know better, the adjustment will be a no-op.
+  long adjustedFrameTimeNs = framePresentationTimeNs;
+  long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+  if (haveSync) {
+    // See if we've advanced to the next frame.
+    if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+      frameCount++;
+      adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+    }
+    if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+      // We're synced and have waited the required number of frames to apply an
+      // adjustment. Calculate the average frame time across all the frames
+      // we've seen since the last sync. This will typically give us a frame
+      // rate at a finer granularity than the frame times themselves (which
+      // often only have millisecond granularity).
+      long averageFrameDurationNs =
+          (framePresentationTimeNs - syncFramePresentationTimeNs) / frameCount;
+      // Project the adjusted frame time forward using the average.
+      long candidateAdjustedFrameTimeNs =
+          adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+      if (isDriftTooLarge(candidateAdjustedFrameTimeNs,
+                          unadjustedReleaseTimeNs)) {
+        haveSync = false;
+      } else {
+        adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+        adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs +
+                                adjustedFrameTimeNs -
+                                syncFramePresentationTimeNs;
+      }
+    } else {
+      // We're synced but haven't waited the required number of frames to apply
+      // an adjustment. Check drift anyway.
+      if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+        haveSync = false;
+      }
+    }
+  }
+
+  // If we need to sync, do so now.
+  if (!haveSync) {
+    syncFramePresentationTimeNs = framePresentationTimeNs;
+    syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+    frameCount = 0;
+    haveSync = true;
+    onSynced();
+  }
+
+  lastFramePresentationTimeUs = framePresentationTimeUs;
+  pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+  if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+    return adjustedReleaseTimeNs;
+  }
+
+  // Find the timestamp of the closest vsync. This is the vsync that we're
+  // targeting.
+  long snappedTimeNs = closestVsync(
+      adjustedReleaseTimeNs, vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+  // Apply an offset so that we release before the target vsync, but after the
+  // previous one.
+  return snappedTimeNs - vsyncOffsetNs;
+}
+
+void VideoFrameReleaseTimeHelper::onSynced() {
+  // Do nothing.
+}
+
+boolean VideoFrameReleaseTimeHelper::isDriftTooLarge(long frameTimeNs,
+                                                     long releaseTimeNs) {
+  long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+  long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+  return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) >
+         MAX_ALLOWED_DRIFT_NS;
+}
+
+static long VideoFrameReleaseTimeHelper::closestVsync(long releaseTime,
+                                                      long sampledVsyncTime,
+                                                      long vsyncDuration) {
+  long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+  long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+  long snappedBeforeNs;
+  long snappedAfterNs;
+  if (releaseTime <= snappedTimeNs) {
+    snappedBeforeNs = snappedTimeNs - vsyncDuration;
+    snappedAfterNs = snappedTimeNs;
+  } else {
+    snappedBeforeNs = snappedTimeNs;
+    snappedAfterNs = snappedTimeNs + vsyncDuration;
+  }
+  long snappedAfterDiff = snappedAfterNs - releaseTime;
+  long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+  return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs
+                                              : snappedBeforeNs;
+}
+
+static double VideoFrameReleaseTimeHelper::getDefaultDisplayRefreshRate(
+    Context context) {
+  WindowManager manager =
+      (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+  return manager.getDefaultDisplay() != null
+             ? manager.getDefaultDisplay().getRefreshRate()
+             : DISPLAY_REFRESH_RATE_UNKNOWN;
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/video_frame_release_time_adjuster.h b/src/starboard/android/shared/video_frame_release_time_adjuster.h
new file mode 100644
index 0000000..a345660
--- /dev/null
+++ b/src/starboard/android/shared/video_frame_release_time_adjuster.h
@@ -0,0 +1,189 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoFrameReleaseTimeAdjuster {
+ public:
+  /**
+   * Constructs an instance that smooths frame release timestamps but does not
+   * align them with the default display's vsync signal.
+   */
+  VideoFrameReleaseTimeHelper();
+
+  /**
+   * Constructs an instance that smooths frame release timestamps and aligns
+   * them with the default display's vsync signal.
+   *
+   * @param context A context from which information about the default display
+   * can be retrieved.
+   */
+  VideoFrameReleaseTimeHelper(Context context);
+
+  /** Enables the helper. */
+  void enable();
+
+  /** Disables the helper. */
+  void disable();
+
+  /**
+   * Adjusts a frame release timestamp.
+   *
+   * @param framePresentationTimeUs The frame's presentation time, in
+   * microseconds.
+   * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in
+   * nanoseconds and in the same time base as {@link System#nanoTime()}.
+   * @return The adjusted frame release timestamp, in nanoseconds and in the
+   * same time base as
+   *     {@link System#nanoTime()}.
+   */
+  long adjustReleaseTime(long framePresentationTimeUs,
+                         long unadjustedReleaseTimeNs);
+
+ protected:
+  void onSynced();
+
+ private:
+  VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate);
+
+  boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs);
+
+  static long closestVsync(long releaseTime,
+                           long sampledVsyncTime,
+                           long vsyncDuration);
+
+  static double getDefaultDisplayRefreshRate(Context context);
+
+  /**
+   * Samples display vsync timestamps. A single instance using a single {@link
+   * Choreographer} is shared by all {@link VideoFrameReleaseTimeHelper}
+   * instances. This is done to avoid a resource leak in the platform on API
+   * levels prior to 23.
+   */
+  class VSyncSampler implements FrameCallback, Handler.Callback {
+   public:
+    static VSyncSampler getInstance() { return INSTANCE; }
+
+    VSyncSampler() {
+      choreographerOwnerThread =
+          new HandlerThread("ChoreographerOwner:Handler");
+      choreographerOwnerThread.start();
+      handler = new Handler(choreographerOwnerThread.getLooper(), this);
+      handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+    }
+
+    /**
+     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is
+     * observing {@link #sampledVsyncTimeNs}, and hence that the value should be
+     * periodically updated.
+     */
+    void addObserver() { handler.sendEmptyMessage(MSG_ADD_OBSERVER); }
+
+    /**
+     * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no
+     * longer observing {@link #sampledVsyncTimeNs}.
+     */
+    void removeObserver() { handler.sendEmptyMessage(MSG_REMOVE_OBSERVER); }
+
+    void doFrame(long vsyncTimeNs) {
+      sampledVsyncTimeNs = vsyncTimeNs;
+      choreographer.postFrameCallbackDelayed(this,
+                                             CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+    }
+
+    boolean handleMessage(Message message) {
+      switch (message.what) {
+        case CREATE_CHOREOGRAPHER: {
+          createChoreographerInstanceInternal();
+          return true;
+        }
+        case MSG_ADD_OBSERVER: {
+          addObserverInternal();
+          return true;
+        }
+        case MSG_REMOVE_OBSERVER: {
+          removeObserverInternal();
+          return true;
+        }
+        default: { return false; }
+      }
+    }
+
+    volatile long sampledVsyncTimeNs;
+
+   private:
+    void createChoreographerInstanceInternal() {
+      choreographer = Choreographer.getInstance();
+    }
+
+    void addObserverInternal() {
+      observerCount++;
+      if (observerCount == 1) {
+        choreographer.postFrameCallback(this);
+      }
+    }
+
+    void removeObserverInternal() {
+      observerCount--;
+      if (observerCount == 0) {
+        choreographer.removeFrameCallback(this);
+        sampledVsyncTimeNs = 0;
+      }
+    }
+
+    static final int CREATE_CHOREOGRAPHER = 0;
+    static final int MSG_ADD_OBSERVER = 1;
+    static final int MSG_REMOVE_OBSERVER = 2;
+
+    static final VSyncSampler INSTANCE = new VSyncSampler();
+
+    final Handler handler;
+    final HandlerThread choreographerOwnerThread;
+    Choreographer choreographer;
+    int observerCount;
+  };
+
+  static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1;
+  static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+  static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+  static final long VSYNC_OFFSET_PERCENTAGE = 80;
+  static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+  static final long NANOS_PER_SECOND = 1000000000L;
+
+  final VSyncSampler vsyncSampler;
+  final boolean useDefaultDisplayVsync;
+  final long vsyncDurationNs;
+  final long vsyncOffsetNs;
+
+  long lastFramePresentationTimeUs;
+  long adjustedLastFrameTimeNs;
+  long pendingAdjustedFrameTimeNs;
+
+  boolean haveSync;
+  long syncUnadjustedReleaseTimeNs;
+  long syncFramePresentationTimeNs;
+  long frameCount;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_VIDEO_FRAME_RELEASE_TIME_ADJUSTER_H_
diff --git a/src/starboard/android/shared/video_render_algorithm.cc b/src/starboard/android/shared/video_render_algorithm.cc
new file mode 100644
index 0000000..639ae01
--- /dev/null
+++ b/src/starboard/android/shared/video_render_algorithm.cc
@@ -0,0 +1,124 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_render_algorithm.h"
+
+#include <algorithm>
+
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/media_common.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+const SbTimeMonotonic kBufferTooLateThreshold = -30 * kSbTimeMillisecond;
+const SbTimeMonotonic kBufferReadyThreshold = 50 * kSbTimeMillisecond;
+
+jlong GetSystemNanoTime() {
+  timespec now;
+  clock_gettime(CLOCK_MONOTONIC, &now);
+  return now.tv_sec * 1000000000LL + now.tv_nsec;
+}
+
+}  // namespace
+
+void VideoRenderAlgorithm::Render(
+    MediaTimeProvider* media_time_provider,
+    std::list<scoped_refptr<VideoFrame>>* frames,
+    VideoRendererSink::DrawFrameCB draw_frame_cb) {
+  SB_DCHECK(media_time_provider);
+  SB_DCHECK(frames);
+  SB_DCHECK(draw_frame_cb);
+
+  while (frames->size() > 0) {
+    if (frames->front()->is_end_of_stream()) {
+      frames->pop_front();
+      SB_DCHECK(frames->empty())
+          << "Expected end of stream output buffer to be the last buffer.";
+      break;
+    }
+
+    bool is_audio_playing;
+    bool is_audio_eos_played;
+    bool is_underflow;
+    SbTime playback_time = media_time_provider->GetCurrentMediaTime(
+        &is_audio_playing, &is_audio_eos_played, &is_underflow);
+    if (!is_audio_playing) {
+      break;
+    }
+
+    jlong early_us = frames->front()->timestamp() - playback_time;
+
+    auto system_time_ns = GetSystemNanoTime();
+    auto unadjusted_frame_release_time_ns =
+        system_time_ns + (early_us * kSbTimeNanosecondsPerMicrosecond);
+
+    auto adjusted_release_time_ns =
+        video_frame_release_time_helper_.AdjustReleaseTime(
+            frames->front()->timestamp(), unadjusted_frame_release_time_ns);
+
+    early_us = (adjusted_release_time_ns - system_time_ns) /
+               kSbTimeNanosecondsPerMicrosecond;
+
+    if (early_us < kBufferTooLateThreshold) {
+      frames->pop_front();
+      ++dropped_frames_;
+    } else if (early_us < kBufferReadyThreshold) {
+      auto status = draw_frame_cb(frames->front(), adjusted_release_time_ns);
+      SB_DCHECK(status == VideoRendererSink::kReleased);
+      frames->pop_front();
+    } else {
+      break;
+    }
+  }
+}
+
+VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::
+    VideoFrameReleaseTimeHelper() {
+  auto* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> j_context(env->CallStarboardObjectMethodOrAbort(
+      "getApplicationContext", "()Landroid/content/Context;"));
+  j_video_frame_release_time_helper_ =
+      env->NewObjectOrAbort("dev/cobalt/media/VideoFrameReleaseTimeHelper",
+                            "(Landroid/content/Context;)V", j_context.Get());
+  j_video_frame_release_time_helper_ =
+      env->ConvertLocalRefToGlobalRef(j_video_frame_release_time_helper_);
+  env->CallVoidMethod(j_video_frame_release_time_helper_, "enable", "()V");
+}
+
+VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::
+    ~VideoFrameReleaseTimeHelper() {
+  SB_DCHECK(j_video_frame_release_time_helper_);
+  auto* env = JniEnvExt::Get();
+  env->CallVoidMethod(j_video_frame_release_time_helper_, "disable", "()V");
+  env->DeleteGlobalRef(j_video_frame_release_time_helper_);
+  j_video_frame_release_time_helper_ = nullptr;
+}
+
+jlong VideoRenderAlgorithm::VideoFrameReleaseTimeHelper::AdjustReleaseTime(
+    jlong frame_presentation_time_us,
+    jlong unadjusted_release_time_ns) {
+  SB_DCHECK(j_video_frame_release_time_helper_);
+  auto* env = JniEnvExt::Get();
+  return env->CallLongMethodOrAbort(
+      j_video_frame_release_time_helper_, "adjustReleaseTime", "(JJ)J",
+      frame_presentation_time_us, unadjusted_release_time_ns);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/video_render_algorithm.h b/src/starboard/android/shared/video_render_algorithm.h
new file mode 100644
index 0000000..927b514
--- /dev/null
+++ b/src/starboard/android/shared/video_render_algorithm.h
@@ -0,0 +1,55 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
+
+#include <list>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/shared/starboard/player/filter/video_renderer_internal.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+class VideoRenderAlgorithm : public ::starboard::shared::starboard::player::
+                                 filter::VideoRenderAlgorithm {
+ public:
+  void Render(MediaTimeProvider* media_time_provider,
+              std::list<scoped_refptr<VideoFrame>>* frames,
+              VideoRendererSink::DrawFrameCB draw_frame_cb) override;
+  int GetDroppedFrames() override { return dropped_frames_; }
+
+ private:
+  class VideoFrameReleaseTimeHelper {
+   public:
+    VideoFrameReleaseTimeHelper();
+    ~VideoFrameReleaseTimeHelper();
+    jlong AdjustReleaseTime(jlong frame_presentation_time_us,
+                            jlong unadjusted_release_time_ns);
+
+   private:
+    jobject j_video_frame_release_time_helper_ = nullptr;
+  };
+
+  VideoFrameReleaseTimeHelper video_frame_release_time_helper_;
+  int dropped_frames_ = 0;
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_VIDEO_RENDER_ALGORITHM_H_
diff --git a/src/starboard/android/shared/video_window.cc b/src/starboard/android/shared/video_window.cc
new file mode 100644
index 0000000..da4bbf0
--- /dev/null
+++ b/src/starboard/android/shared/video_window.cc
@@ -0,0 +1,207 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/video_window.h"
+
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <EGL/egl.h>
+#include <GLES2/gl2.h>
+#include <jni.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/condition_variable.h"
+#include "starboard/configuration.h"
+#include "starboard/log.h"
+#include "starboard/shared/gles/gl_call.h"
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+namespace {
+
+// Global pointer to the single video surface.
+jobject g_j_video_surface = NULL;
+// Global pointer to the single video window.
+ANativeWindow* g_native_video_window = NULL;
+
+// Facilitate synchronization of punch-out videos.
+SbMutex g_bounds_updates_mutex = SB_MUTEX_INITIALIZER;
+SbConditionVariable g_bounds_updates_condition =
+    SB_CONDITION_VARIABLE_INITIALIZER;
+int g_bounds_updates_needed = 0;
+int g_bounds_updates_scheduled = 0;
+
+}  // namespace
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged(
+    JNIEnv* env,
+    jobject unused_this,
+    jobject surface) {
+  if (g_j_video_surface) {
+    // TODO: Ensure that the decoder isn't still using the surface.
+    env->DeleteGlobalRef(g_j_video_surface);
+  }
+  if (g_native_video_window) {
+    // TODO: Ensure that the decoder isn't still using the window.
+    ANativeWindow_release(g_native_video_window);
+  }
+  if (surface) {
+    g_j_video_surface = env->NewGlobalRef(surface);
+    g_native_video_window = ANativeWindow_fromSurface(env, surface);
+  } else {
+    g_j_video_surface = NULL;
+    g_native_video_window = NULL;
+  }
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnLayoutNeeded() {
+  SbMutexAcquire(&g_bounds_updates_mutex);
+  ++g_bounds_updates_needed;
+  SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnLayoutScheduled() {
+  SbMutexAcquire(&g_bounds_updates_mutex);
+  ++g_bounds_updates_scheduled;
+  SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_media_VideoSurfaceView_nativeOnGlobalLayout() {
+  SbMutexAcquire(&g_bounds_updates_mutex);
+  g_bounds_updates_needed -= g_bounds_updates_scheduled;
+  g_bounds_updates_scheduled = 0;
+  if (g_bounds_updates_needed <= 0) {
+    g_bounds_updates_needed = 0;
+    SbConditionVariableSignal(&g_bounds_updates_condition);
+  }
+  SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+jobject GetVideoSurface() {
+  return g_j_video_surface;
+}
+
+ANativeWindow* GetVideoWindow() {
+  return g_native_video_window;
+}
+
+void ClearVideoWindow() {
+  if (!g_native_video_window) {
+    SB_LOG(INFO) << "Tried to clear video window when it was null.";
+    return;
+  }
+
+  EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+  eglInitialize(display, NULL, NULL);
+  if (display == EGL_NO_DISPLAY) {
+    SB_DLOG(ERROR) << "Found no EGL display in ClearVideoWindow";
+    return;
+  }
+
+  const EGLint kAttributeList[] = {
+      EGL_RED_SIZE,
+      8,
+      EGL_GREEN_SIZE,
+      8,
+      EGL_BLUE_SIZE,
+      8,
+      EGL_ALPHA_SIZE,
+      8,
+      EGL_RENDERABLE_TYPE,
+      EGL_OPENGL_ES2_BIT,
+      EGL_NONE,
+      0,
+      EGL_NONE,
+  };
+
+  // First, query how many configs match the given attribute list.
+  EGLint num_configs = 0;
+  EGL_CALL(eglChooseConfig(display, kAttributeList, NULL, 0, &num_configs));
+  SB_DCHECK(num_configs != 0);
+
+  // Allocate space to receive the matching configs and retrieve them.
+  EGLConfig* configs = new EGLConfig[num_configs];
+  EGL_CALL(eglChooseConfig(display, kAttributeList, configs, num_configs,
+                           &num_configs));
+
+  EGLNativeWindowType native_window =
+      static_cast<EGLNativeWindowType>(g_native_video_window);
+  EGLConfig config;
+
+  // Find the first config that successfully allow a window surface to be
+  // created.
+  EGLSurface surface;
+  for (int config_number = 0; config_number < num_configs; ++config_number) {
+    config = configs[config_number];
+    surface = eglCreateWindowSurface(display, config, native_window, NULL);
+    if (eglGetError() == EGL_SUCCESS)
+      break;
+  }
+  if (surface == EGL_NO_SURFACE) {
+    SB_DLOG(ERROR) << "Found no EGL surface in ClearVideoWindow";
+    return;
+  }
+  SB_DCHECK(surface != EGL_NO_SURFACE);
+
+  delete[] configs;
+
+  // Create an OpenGL ES 2.0 context.
+  EGLContext context = EGL_NO_CONTEXT;
+  EGLint context_attrib_list[] = {
+      EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE,
+  };
+  context =
+      eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrib_list);
+  SB_DCHECK(eglGetError() == EGL_SUCCESS);
+  SB_DCHECK(context != EGL_NO_CONTEXT);
+
+  /* connect the context to the surface */
+  EGL_CALL(eglMakeCurrent(display, surface, surface, context));
+
+  GL_CALL(glClearColor(0, 0, 0, 1));
+  GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
+  GL_CALL(glFlush());
+  EGL_CALL(eglSwapBuffers(display, surface));
+
+  // Cleanup all used resources.
+  EGL_CALL(
+      eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
+  EGL_CALL(eglDestroyContext(display, context));
+  EGL_CALL(eglDestroySurface(display, surface));
+  EGL_CALL(eglTerminate(display));
+}
+
+void WaitForVideoBoundsUpdate() {
+  SbMutexAcquire(&g_bounds_updates_mutex);
+  if (g_bounds_updates_needed > 0) {
+    // Use a timed wait to deal with possible race conditions in which
+    // suspend occurs in the middle of a bounds update.
+    SbConditionVariableWaitTimed(&g_bounds_updates_condition,
+                                 &g_bounds_updates_mutex,
+                                 100 * kSbTimeMillisecond);
+    g_bounds_updates_needed = 0;
+    g_bounds_updates_scheduled = 0;
+  }
+  SbMutexRelease(&g_bounds_updates_mutex);
+}
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
diff --git a/src/starboard/android/shared/video_window.h b/src/starboard/android/shared/video_window.h
new file mode 100644
index 0000000..39cb33f
--- /dev/null
+++ b/src/starboard/android/shared/video_window.h
@@ -0,0 +1,43 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
+#define STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
+
+#include <android/native_window.h>
+#include <jni.h>
+
+namespace starboard {
+namespace android {
+namespace shared {
+
+// Returns the surface which video should be rendered.  This is the surface
+// that owns the native window returned by |GetVideoWindow|.
+jobject GetVideoSurface();
+
+// Returns the native window into which video should be rendered.
+ANativeWindow* GetVideoWindow();
+
+// Clear the video window by painting it Black.  This function is safe to call
+// regardless of whether the video window has been initialized or not.
+void ClearVideoWindow();
+
+// Wait for all outstanding adjustments of video bounds before returning.
+void WaitForVideoBoundsUpdate();
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_VIDEO_WINDOW_H_
diff --git a/src/starboard/android/shared/window_create.cc b/src/starboard/android/shared/window_create.cc
new file mode 100644
index 0000000..ec3ecf7
--- /dev/null
+++ b/src/starboard/android/shared/window_create.cc
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+SbWindow SbWindowCreate(const SbWindowOptions* options) {
+  return starboard::android::shared::ApplicationAndroid::Get()->CreateWindow(
+      options);
+}
diff --git a/src/starboard/android/shared/window_destroy.cc b/src/starboard/android/shared/window_destroy.cc
new file mode 100644
index 0000000..afb0c7e
--- /dev/null
+++ b/src/starboard/android/shared/window_destroy.cc
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+bool SbWindowDestroy(SbWindow window) {
+  return starboard::android::shared::ApplicationAndroid::Get()->DestroyWindow(
+      window);
+}
diff --git a/src/starboard/android/shared/window_get_platform_handle.cc b/src/starboard/android/shared/window_get_platform_handle.cc
new file mode 100644
index 0000000..60fd357
--- /dev/null
+++ b/src/starboard/android/shared/window_get_platform_handle.cc
@@ -0,0 +1,25 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/window.h"
+
+#include "starboard/android/shared/window_internal.h"
+
+void* SbWindowGetPlatformHandle(SbWindow window) {
+  if (!SbWindowIsValid(window)) {
+    return NULL;
+  }
+  // EGLNativeWindowType and ANativeWindow* are the same.
+  return window->native_window;
+}
diff --git a/src/starboard/android/shared/window_get_size.cc b/src/starboard/android/shared/window_get_size.cc
new file mode 100644
index 0000000..c4bbebd
--- /dev/null
+++ b/src/starboard/android/shared/window_get_size.cc
@@ -0,0 +1,51 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android/native_window.h>
+
+#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/android/shared/jni_utils.h"
+#include "starboard/android/shared/window_internal.h"
+#include "starboard/log.h"
+#include "starboard/window.h"
+
+using starboard::android::shared::JniEnvExt;
+using starboard::android::shared::ScopedLocalJavaRef;
+
+bool SbWindowGetSize(SbWindow window, SbWindowSize* size) {
+  if (!SbWindowIsValid(window)) {
+    SB_DLOG(ERROR) << __FUNCTION__ << ": Invalid window.";
+    return false;
+  }
+
+  size->width = ANativeWindow_getWidth(window->native_window);
+  size->height = ANativeWindow_getHeight(window->native_window);
+
+  JniEnvExt* env = JniEnvExt::Get();
+  ScopedLocalJavaRef<jobject> display_size(
+      env->CallStarboardObjectMethodOrAbort("getDisplaySize",
+                                           "()Landroid/util/Size;"));
+  int display_width =
+      env->CallIntMethodOrAbort(display_size.Get(), "getWidth", "()I");
+  int display_height =
+      env->CallIntMethodOrAbort(display_size.Get(), "getHeight", "()I");
+
+  // In the off chance we have non-square pixels, use the max ratio so the
+  // highest quality video suitable to the device gets selected.
+  size->video_pixel_ratio = std::max(
+      static_cast<float>(display_width) / size->width,
+      static_cast<float>(display_height) / size->height);
+
+  return true;
+}
diff --git a/src/starboard/android/shared/window_internal.h b/src/starboard/android/shared/window_internal.h
new file mode 100644
index 0000000..24a6ed7
--- /dev/null
+++ b/src/starboard/android/shared/window_internal.h
@@ -0,0 +1,26 @@
+// Copyright 2017 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
+#define STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
+
+#include <android/native_window.h>
+
+#include "starboard/window.h"
+
+struct SbWindowPrivate {
+  ANativeWindow* native_window;
+};
+
+#endif  // STARBOARD_ANDROID_SHARED_WINDOW_INTERNAL_H_
diff --git a/src/starboard/android/x86/atomic_public.h b/src/starboard/android/x86/atomic_public.h
new file mode 100644
index 0000000..640dc99
--- /dev/null
+++ b/src/starboard/android/x86/atomic_public.h
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/atomic_public.h"
diff --git a/src/starboard/android/x86/configuration_public.h b/src/starboard/android/x86/configuration_public.h
new file mode 100644
index 0000000..e36dc6a
--- /dev/null
+++ b/src/starboard/android/x86/configuration_public.h
@@ -0,0 +1,22 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// The Starboard configuration for Android X86. Other devices will have
+// specific Starboard implementations, even if they ultimately are running some
+// version of Android.
+
+// Other source files should never include this header directly, but should
+// include the generic "starboard/configuration.h" instead.
+
+#include "starboard/android/shared/configuration_public.h"
diff --git a/src/starboard/android/x86/gyp_configuration.gypi b/src/starboard/android/x86/gyp_configuration.gypi
new file mode 100644
index 0000000..3c5d162
--- /dev/null
+++ b/src/starboard/android/x86/gyp_configuration.gypi
@@ -0,0 +1,41 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{
+  'variables': {
+    'target_arch': 'ia32',
+  },
+
+  'target_defaults': {
+    'default_configuration': 'android-x86_debug',
+    'configurations': {
+      'android-x86_debug': {
+        'inherit_from': ['debug_base'],
+      },
+      'android-x86_devel': {
+        'inherit_from': ['devel_base'],
+      },
+      'android-x86_qa': {
+        'inherit_from': ['qa_base'],
+      },
+      'android-x86_gold': {
+        'inherit_from': ['gold_base'],
+      },
+    }, # end of configurations
+  },
+
+  'includes': [
+    '../shared/gyp_configuration.gypi',
+  ],
+}
diff --git a/src/starboard/android/x86/gyp_configuration.py b/src/starboard/android/x86/gyp_configuration.py
new file mode 100644
index 0000000..f31ab4b
--- /dev/null
+++ b/src/starboard/android/x86/gyp_configuration.py
@@ -0,0 +1,20 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android x86 platform build configuration."""
+
+from starboard.android.shared import gyp_configuration as shared_configuration
+
+
+def CreatePlatformConfig():
+  return shared_configuration.AndroidConfiguration('android-x86', 'x86')
diff --git a/src/starboard/android/x86/starboard_platform.gyp b/src/starboard/android/x86/starboard_platform.gyp
new file mode 100644
index 0000000..7e4cdc2
--- /dev/null
+++ b/src/starboard/android/x86/starboard_platform.gyp
@@ -0,0 +1,16 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform.gypi' ],
+}
diff --git a/src/starboard/android/x86/starboard_platform_tests.gyp b/src/starboard/android/x86/starboard_platform_tests.gyp
new file mode 100644
index 0000000..f7f55a3
--- /dev/null
+++ b/src/starboard/android/x86/starboard_platform_tests.gyp
@@ -0,0 +1,16 @@
+# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+{
+  'includes': [ '../shared/starboard_platform_tests.gypi' ],
+}
diff --git a/src/starboard/android/x86/thread_types_public.h b/src/starboard/android/x86/thread_types_public.h
new file mode 100644
index 0000000..1416655
--- /dev/null
+++ b/src/starboard/android/x86/thread_types_public.h
@@ -0,0 +1,15 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/android/shared/thread_types_public.h"
diff --git a/src/starboard/nplb/socket_get_interface_address_test.cc b/src/starboard/nplb/socket_get_interface_address_test.cc
index f925495..bb33ada 100644
--- a/src/starboard/nplb/socket_get_interface_address_test.cc
+++ b/src/starboard/nplb/socket_get_interface_address_test.cc
@@ -83,16 +83,16 @@
   SbMemorySet(&source, kInvalidByte, sizeof(source));
 
   EXPECT_TRUE(SbSocketGetInterfaceAddress(&destination, &source, NULL));
-  EXPECT_TRUE(source.type == GetAddressType());
+  EXPECT_EQ(GetAddressType(), source.type);
   EXPECT_TRUE(SbSocketGetInterfaceAddress(&destination, &source, &netmask));
 
   EXPECT_FALSE(IsLocalhost(&source));
 
   // A netmask that starts with 0 is likely incorrect.
   EXPECT_TRUE(netmask.address[0] & 0x8);
-  EXPECT_EQ(source.type, GetAddressType());
-  EXPECT_EQ(netmask.type, GetAddressType());
-  EXPECT_EQ(source.port, 0);
+  EXPECT_EQ(GetAddressType(), source.type);
+  EXPECT_EQ(GetAddressType(), netmask.type);
+  EXPECT_EQ(0, source.port);
 }
 
 TEST_P(SbSocketGetInterfaceAddressTest, SunnyDaySourceForDestination) {
@@ -125,11 +125,11 @@
   SbMemorySet(&invalid_address, kInvalidByte, sizeof(source));
   SbSocketGetInterfaceAddress(&destination_address, &source, &netmask);
 
-  EXPECT_TRUE(source.type == GetAddressType());
-  EXPECT_NE(source.port, 0);
+  EXPECT_EQ(GetAddressType(), source.type);
+  EXPECT_NE(0, source.port);
   // A netmask that starts with 0 is likely incorrect.
   EXPECT_TRUE(netmask.address[0] & 0x8);
-  EXPECT_EQ(netmask.type, GetAddressType());
+  EXPECT_EQ(GetAddressType(), netmask.type);
   EXPECT_NE(0, SbMemoryCompare(source.address, invalid_address.address,
                                SB_ARRAY_SIZE(source.address)));
   EXPECT_NE(0, SbMemoryCompare(netmask.address, invalid_address.address,
@@ -163,7 +163,7 @@
   SbMemorySet(&invalid_address, kInvalidByte, sizeof(invalid_address));
 
   EXPECT_TRUE(SbSocketGetInterfaceAddress(&destination, &source, NULL));
-  EXPECT_EQ(source.type, GetAddressType());
+  EXPECT_EQ(GetAddressType(), source.type);
   EXPECT_TRUE(SbSocketGetInterfaceAddress(&destination, &source, &netmask));
   EXPECT_FALSE(IsLocalhost(&source));
   EXPECT_FALSE(IsUnspecified(&source));
diff --git a/src/starboard/nplb/speech_recognizer_helper.h b/src/starboard/nplb/speech_recognizer_helper.h
index a7ab9d8..cba631c 100644
--- a/src/starboard/nplb/speech_recognizer_helper.h
+++ b/src/starboard/nplb/speech_recognizer_helper.h
@@ -27,50 +27,24 @@
 
 #if SB_HAS(SPEECH_RECOGNIZER) && SB_API_VERSION >= 5
 
-class EventMock : public RefCounted<EventMock> {
- public:
-  MOCK_METHOD0(OnEvent, void(void));
-};
-
 class SpeechRecognizerTest : public ::testing::Test {
  public:
-  SpeechRecognizerTest()
-      : handler_(), test_mock_(new ::testing::StrictMock<EventMock>()) {
+  SpeechRecognizerTest() : handler_() {
     handler_.on_speech_detected = OnSpeechDetected;
     handler_.on_error = OnError;
     handler_.on_results = OnResults;
     handler_.context = this;
   }
 
-  static void OnSpeechDetected(void* context, bool detected) {
-    SpeechRecognizerTest* test = static_cast<SpeechRecognizerTest*>(context);
-    test->OnSignalEvent();
-  }
-  static void OnError(void* context, SbSpeechRecognizerError error) {
-    SpeechRecognizerTest* test = static_cast<SpeechRecognizerTest*>(context);
-    test->OnSignalEvent();
-  }
+  static void OnSpeechDetected(void* context, bool detected) {}
+  static void OnError(void* context, SbSpeechRecognizerError error) {}
   static void OnResults(void* context,
                         SbSpeechResult* results,
                         int results_size,
-                        bool is_final) {
-    SpeechRecognizerTest* test = static_cast<SpeechRecognizerTest*>(context);
-    test->OnSignalEvent();
-  }
+                        bool is_final) {}
 
-  SbSpeechRecognizerHandler* handler() { return &handler_; }
-
-  EventMock& test_mock() { return *test_mock_.get(); }
-
-  void Wait() {
-    if (!event_semaphore_.TakeWait(kWaitTime)) {
-      SB_LOG(WARNING) << "Waiting for recognizer event to come, but timeout!";
-    }
-  }
-
-  void OnSignalEvent() {
-    test_mock_->OnEvent();
-    event_semaphore_.Put();
+  SbSpeechRecognizerHandler* handler() {
+    return &handler_;
   }
 
  protected:
@@ -84,13 +58,8 @@
 
  private:
   const SbTime kTearDownTime = 10 * kSbTimeMillisecond;
-  const SbTime kWaitTime = 600 * kSbTimeMillisecond;
 
   SbSpeechRecognizerHandler handler_;
-
-  starboard::Semaphore event_semaphore_;
-
-  const scoped_refptr<EventMock> test_mock_;
 };
 
 #endif  // SB_HAS(SPEECH_RECOGNIZER) && SB_API_VERSION >= 5
diff --git a/src/starboard/nplb/speech_recognizer_start_test.cc b/src/starboard/nplb/speech_recognizer_start_test.cc
index 4f37fe4..15922f6 100644
--- a/src/starboard/nplb/speech_recognizer_start_test.cc
+++ b/src/starboard/nplb/speech_recognizer_start_test.cc
@@ -31,8 +31,6 @@
     return;
   }
 
-  EXPECT_CALL(test_mock(), OnEvent()).Times(testing::AtLeast(1));
-  Wait();
   SbSpeechRecognizerStop(recognizer);
   SbSpeechRecognizerDestroy(recognizer);
 }
@@ -47,8 +45,6 @@
     return;
   }
 
-  EXPECT_CALL(test_mock(), OnEvent()).Times(testing::AtLeast(1));
-  Wait();
   SbSpeechRecognizerStop(recognizer);
   SbSpeechRecognizerDestroy(recognizer);
 }
@@ -63,8 +59,6 @@
     return;
   }
 
-  EXPECT_CALL(test_mock(), OnEvent()).Times(testing::AtLeast(1));
-  Wait();
   SbSpeechRecognizerStop(recognizer);
   SbSpeechRecognizerDestroy(recognizer);
 }
@@ -79,8 +73,6 @@
     return;
   }
 
-  EXPECT_CALL(test_mock(), OnEvent()).Times(testing::AtLeast(1));
-  Wait();
   SbSpeechRecognizerStop(recognizer);
   SbSpeechRecognizerDestroy(recognizer);
 }
@@ -95,12 +87,10 @@
     return;
   }
 
-  EXPECT_CALL(test_mock(), OnEvent()).Times(testing::AtLeast(1));
   bool success = SbSpeechRecognizerStart(recognizer, &configuration);
   EXPECT_FALSE(success);
   success = SbSpeechRecognizerStart(recognizer, &configuration);
   EXPECT_FALSE(success);
-  Wait();
   SbSpeechRecognizerDestroy(recognizer);
 }