Import Cobalt 19.master.0.194710

Includes the following patches:
  https://cobalt-review.googlesource.com/c/cobalt/+/5190
    by errong.leng@samsung.com
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..8e8b2dc
--- /dev/null
+++ b/src/starboard/android/shared/application_android.cc
@@ -0,0 +1,500 @@
+// 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));
+}
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+
+void ApplicationAndroid::SbWindowShowOnScreenKeyboard(SbWindow window,
+                                                      const char* input_text,
+                                                      int ticket) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject j_keyboard_editor = env->CallStarboardObjectMethodOrAbort(
+      "getKeyboardEditor", "()Ldev/cobalt/coat/KeyboardEditor;");
+  env->CallVoidMethodOrAbort(j_keyboard_editor, "showKeyboard", "()V");
+  // TODO: Fire kSbEventTypeWindowSizeChange and
+  // kSbEventTypeOnScreenKeyboardShown if necessary.
+  return;
+}
+
+void ApplicationAndroid::SbWindowHideOnScreenKeyboard(SbWindow window,
+                                                      int ticket) {
+  JniEnvExt* env = JniEnvExt::Get();
+  jobject j_keyboard_editor = env->CallStarboardObjectMethodOrAbort(
+      "getKeyboardEditor", "()Ldev/cobalt/coat/KeyboardEditor;");
+  env->CallVoidMethodOrAbort(j_keyboard_editor, "hideKeyboard", "()V");
+  // TODO: Fire kSbEventTypeWindowSizeChange and
+  // kSbEventTypeOnScreenKeyboardHidden if necessary.
+  return;
+}
+
+extern "C" SB_EXPORT_PLATFORM void
+Java_dev_cobalt_coat_KeyboardInputConnection_nativeSendText(
+    JniEnvExt* env,
+    jobject unused_clazz,
+    jstring text) {
+  if (text) {
+    std::string utf_str = env->GetStringStandardUTFOrAbort(text);
+    ApplicationAndroid::Get()->SbWindowSendInputEvent(utf_str.c_str());
+  }
+}
+
+void DeleteSbInputDataWithText(void* ptr) {
+  SbInputData* data = static_cast<SbInputData*>(ptr);
+  const char* input_text = data->input_text;
+  data->input_text = NULL;
+  delete input_text;
+  ApplicationAndroid::DeleteDestructor<SbInputData>(ptr);
+}
+
+void ApplicationAndroid::SbWindowSendInputEvent(const char* input_text) {
+  char* text = SbStringDuplicate(input_text);
+  SbInputData* data = new SbInputData();
+  SbMemorySet(data, 0, sizeof(*data));
+  data->window = window_;
+  data->type = kSbInputEventTypeInput;
+  data->device_type = kSbInputDeviceTypeOnScreenKeyboard;
+  data->input_text = text;
+  Inject(new Event(kSbEventTypeInput, data, &DeleteSbInputDataWithText));
+  return;
+}
+
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
+
+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..3631e03
--- /dev/null
+++ b/src/starboard/android/shared/application_android.h
@@ -0,0 +1,131 @@
+// 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/android/shared/jni_env_ext.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);
+
+  void SbWindowShowOnScreenKeyboard(SbWindow window,
+                                    const char* input_text,
+                                    int ticket);
+  void SbWindowHideOnScreenKeyboard(SbWindow window, int ticket);
+  void SbWindowSendInputEvent(const char* input_text);
+
+ 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..8bf09e2
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/configuration.py
@@ -0,0 +1,78 @@
+# 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 __init__(self, platform_configuration, application_name,
+               application_directory):
+    super(CobaltAndroidConfiguration, self).__init__(
+        platform_configuration, application_name, application_directory)
+
+  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': [
+          'DialHttpServerTest.AllOtherRequests',
+          'DialHttpServerTest.CallbackExceptionInServiceHandler',
+          'DialHttpServerTest.CallbackHandleRequestReturnsFalse',
+          'DialHttpServerTest.CallbackNormalTest',
+          'DialHttpServerTest.CurrentRunningAppRedirect',
+          'DialHttpServerTest.SendManifest',
+          '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..890d939
--- /dev/null
+++ b/src/starboard/android/shared/configuration_public.h
@@ -0,0 +1,454 @@
+// 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 ----------------------------------------------
+
+// Do not use <unordered_map> and <unordered_set> for the hash table types.
+#define SB_HAS_STD_UNORDERED_HASH 0
+
+// 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
+
+// --- 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..10f6c0c
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.gypi
@@ -0,0 +1,133 @@
+# 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,
+
+    # Define platform specific compiler and linker flags.
+    # Refer to base.gypi for a list of all available variables.
+    'compiler_flags_host': [
+      '-O2',
+    ],
+    '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': [
+      ['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': {
+    'target_conditions': [
+      ['sb_pedantic_warnings==1', {
+        'cflags': [
+          '-Wall',
+          '-Wextra',
+          '-Wunreachable-code',
+          # Don't get pedantic about warnings from base macros. These must be
+          # disabled after the -Wall above, so this has to be done here rather
+          # than in the platform's target toolchain.
+          # TODO: Rebase base and use static_assert instead of COMPILE_ASSERT
+          '-Wno-unused-local-typedef',  # COMPILE_ASSERT
+          '-Wno-missing-field-initializers',  # LAZY_INSTANCE_INITIALIZER
+        ],
+      }],
+      ['_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..5c78367
--- /dev/null
+++ b/src/starboard/android/shared/gyp_configuration.py
@@ -0,0 +1,302 @@
+# 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
+from subprocess import call
+
+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 starboard.tools.toolchain import ar
+from starboard.tools.toolchain import bash
+from starboard.tools.toolchain import clang
+from starboard.tools.toolchain import clangxx
+from starboard.tools.toolchain import cp
+from starboard.tools.toolchain import touch
+
+_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')
+
+# Maps the Android ABI to the name of the toolchain.
+_ABI_TOOLCHAIN_NAME = {
+    'x86': 'i686-linux-android',
+    'armeabi': 'arm-linux-androideabi',
+    'armeabi-v7a': 'arm-linux-androideabi',
+    'arm64-v8a': 'aarch64-linux-android',
+}
+
+
+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._target_toolchain = None
+    self._host_toolchain = None
+
+    self.AppendApplicationConfigurationPath(os.path.dirname(__file__))
+
+    self.android_abi = 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 = {}
+
+    # 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 GetTargetToolchain(self):
+    if not self._target_toolchain:
+      tool_prefix = os.path.join(
+          sdk_utils.GetToolsPath(self.android_abi), 'bin',
+          _ABI_TOOLCHAIN_NAME[self.android_abi] + '-')
+      cc_path = tool_prefix + 'clang'
+      cxx_path = tool_prefix + 'clang++'
+      ar_path = tool_prefix + 'ar'
+      clang_flags = [
+          # We'll pretend not to be Linux, but Starboard instead.
+          '-U__linux__',
+
+          # libwebp uses the cpufeatures library to detect ARM NEON support
+          '-I{}/sources/android/cpufeatures'.format(self.android_ndk_home),
+
+          # Mimic build/cmake/android.toolchain.cmake in the Android NDK.
+          '-ffunction-sections',
+          '-funwind-tables',
+          '-fstack-protector-strong',
+          '-no-canonical-prefixes',
+
+          # Other flags
+          '-fsigned-char',
+          '-fno-limit-debug-info',
+          '-fno-exceptions',
+          '-fcolor-diagnostics',
+          '-fno-strict-aliasing',  # See http://crbug.com/32204
+
+          # Default visibility is hidden to enable dead stripping.
+          '-fvisibility=hidden',
+          # Any warning will stop the build.
+          '-Werror',
+
+          # 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',
+
+          # 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',
+          # Skia doesn't use overrides.
+          '-Wno-inconsistent-missing-override',
+          # shifting a negative signed value is undefined
+          '-Wno-shift-negative-value',
+          # Don't warn for implicit sign conversions. (in v8 and protobuf)
+          '-Wno-sign-conversion',
+      ]
+      clang_defines = [
+          # Enable compile-time decisions based on the ABI
+          'ANDROID_ABI={}'.format(self.android_abi),
+          # -DANDROID is an argument to some ifdefs in the NDK's eglplatform.h
+          'ANDROID',
+          # Cobalt on Linux flag
+          'COBALT_LINUX',
+          # So that we get the PRI* macros from inttypes.h
+          '__STDC_FORMAT_MACROS',
+          # Enable GNU extensions to get prototypes like ffsl.
+          '_GNU_SOURCE=1',
+          # 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',
+      ]
+      linker_flags = [
+          # Use the static LLVM libc++.
+          '-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',
+
+          # Wrapper synchronizes punch-out video bounds with the UI frame.
+          '-Wl,--wrap=eglSwapBuffers',
+      ]
+      self._target_toolchain = [
+          clang.CCompiler(
+              path=cc_path, defines=clang_defines, extra_flags=clang_flags),
+          clang.CxxCompiler(
+              path=cxx_path,
+              defines=clang_defines,
+              extra_flags=clang_flags + [
+                  '-std=c++11',
+              ]),
+          clang.AssemblerWithCPreprocessor(
+              path=cc_path, defines=clang_defines, extra_flags=clang_flags),
+          ar.StaticThinLinker(path=ar_path),
+          ar.StaticLinker(path=ar_path),
+          clangxx.SharedLibraryLinker(path=cxx_path, extra_flags=linker_flags),
+          clangxx.ExecutableLinker(path=cxx_path, extra_flags=linker_flags),
+      ]
+    return self._target_toolchain
+
+  def GetHostToolchain(self):
+    if not self._host_toolchain:
+      cc_path = self.host_compiler_environment['CC_host'],
+      cxx_path = self.host_compiler_environment['CXX_host']
+      self._host_toolchain = [
+          clang.CCompiler(path=cc_path),
+          clang.CxxCompiler(path=cxx_path),
+          clang.AssemblerWithCPreprocessor(path=cc_path),
+          ar.StaticThinLinker(),
+          ar.StaticLinker(),
+          clangxx.ExecutableLinker(path=cxx_path),
+          clangxx.SharedLibraryLinker(path=cxx_path),
+          cp.Copy(),
+          touch.Stamp(),
+          bash.Shell(),
+      ]
+    return self._host_toolchain
+
+  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': [
+          'SbAudioSinkTest.AllFramesConsumed',
+          'SbAudioSinkTest.SomeFramesConsumed',
+          'SbAudioSinkTest.Underflow',
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDayDestination/0',
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDaySourceForDestination/0',
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDaySourceForDestination/1',
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDaySourceNotLoopback/0',
+          'SbSocketBindTest.SunnyDayLocalInterface',
+          'SbSocketGetLocalAddressTest.SunnyDayBoundSpecified',
+          'SpeechRecognizerTest.StartIsCalledMultipleTimes',
+          'SpeechRecognizerTest.StartRecognizerWith10MaxAlternatives',
+          'SpeechRecognizerTest.StartRecognizerWithContinuousRecognition',
+          'SpeechRecognizerTest.StartRecognizerWithInterimResults',
+          'SpeechRecognizerTest.StartTestSunnyDay',
+      ],
+      '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..b504ae6
--- /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/common.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..7db4b61
--- /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 = SbPlayerPrivate::CreateInstance(
+      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..025e8df
--- /dev/null
+++ b/src/starboard/android/shared/sdk_utils.py
@@ -0,0 +1,348 @@
+# 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 _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..fe75115
--- /dev/null
+++ b/src/starboard/android/shared/socket_get_interface_address.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 "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;
+
+}  // 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.
+// Also, currently, we assume netmask is 255.255.255.0 in IPv4, and
+// has a prefix length of 64 for IPv6.
+// 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 == NULL) {
+    return false;
+  }
+
+  SbMemorySet(out_source_address->address, 0,
+              sizeof(out_source_address->address));
+  out_source_address->port = 0;
+
+  JniEnvExt* env = JniEnvExt::Get();
+
+  jbyteArray s = (jbyteArray)env->CallStarboardObjectMethodOrAbort(
+      "getLocalInterfaceAddress", "()[B");
+  if (s == NULL) {
+    return false;
+  }
+
+  jint sz = env->GetArrayLength(s);
+  if (sz > sizeof(out_source_address->address)) {
+    // This should never happen
+    SB_LOG(ERROR) << "SbSocketGetInterfaceAddress address too big";
+    return false;
+  }
+  switch (sz) {
+    case 4:
+      out_source_address->type = kSbSocketAddressTypeIpv4;
+      if (out_netmask) {
+        out_netmask->address[0] = 255;
+        out_netmask->address[1] = 255;
+        out_netmask->address[2] = 255;
+        out_netmask->address[3] = 0;
+        out_netmask->type = kSbSocketAddressTypeIpv4;
+      }
+      break;
+    default:
+      out_source_address->type = kSbSocketAddressTypeIpv6;
+      if (out_netmask) {
+        for (int i = 0; i < kDefaultPrefixLength; ++i) {
+          out_netmask->address[i] = 0xff;
+        }
+        for (int i = kDefaultPrefixLength; i < net::kIPv6AddressSize; ++i) {
+          out_netmask->address[i] = 0;
+        }
+        out_netmask->type = kSbSocketAddressTypeIpv6;
+      }
+      break;
+  }
+
+  jbyte* bytes = env->GetByteArrayElements(s, NULL);
+  SB_CHECK(bytes) << "GetByteArrayElements failed";
+  SbMemoryCopy(out_source_address->address, bytes, sz);
+  env->ReleaseByteArrayElements(s, bytes, JNI_ABORT);
+
+  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..64bcca9
--- /dev/null
+++ b/src/starboard/android/shared/starboard_platform.gypi
@@ -0,0 +1,486 @@
+# 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/android/shared/window_blur_on_screen_keyboard.cc',
+        '<(DEPTH)/starboard/android/shared//window_focus_on_screen_keyboard.cc',
+        '<(DEPTH)/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc',
+        '<(DEPTH)/starboard/android/shared/window_hide_on_screen_keyboard.cc',
+        '<(DEPTH)/starboard/android/shared/window_is_on_screen_keyboard_shown.cc',
+        '<(DEPTH)/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc',
+        '<(DEPTH)/starboard/android/shared/window_show_on_screen_keyboard.cc',
+        '<(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',
+        '<(DEPTH)/starboard/shared/stub/thread_context_get_pointer.cc',
+        '<(DEPTH)/starboard/shared/stub/thread_sampler_create.cc',
+        '<(DEPTH)/starboard/shared/stub/thread_sampler_destroy.cc',
+        '<(DEPTH)/starboard/shared/stub/thread_sampler_freeze.cc',
+        '<(DEPTH)/starboard/shared/stub/thread_sampler_is_supported.cc',
+        '<(DEPTH)/starboard/shared/stub/thread_sampler_thaw.cc',
+        '<(DEPTH)/starboard/shared/stub/window_get_diagonal_size_in_inches.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_blur_on_screen_keyboard.cc b/src/starboard/android/shared/window_blur_on_screen_keyboard.cc
new file mode 100644
index 0000000..fe84c62
--- /dev/null
+++ b/src/starboard/android/shared/window_blur_on_screen_keyboard.cc
@@ -0,0 +1,22 @@
+// 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/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowBlurOnScreenKeyboard(SbWindow window, int ticket) {
+  // Stub.
+  return;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
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_focus_on_screen_keyboard.cc b/src/starboard/android/shared/window_focus_on_screen_keyboard.cc
new file mode 100644
index 0000000..00dea03
--- /dev/null
+++ b/src/starboard/android/shared/window_focus_on_screen_keyboard.cc
@@ -0,0 +1,22 @@
+// 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/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowFocusOnScreenKeyboard(SbWindow window, int ticket) {
+  // Stub.
+  return;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc b/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc
new file mode 100644
index 0000000..90bcf2f
--- /dev/null
+++ b/src/starboard/android/shared/window_get_on_screen_keyboard_bounding_rect.cc
@@ -0,0 +1,23 @@
+// 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/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+bool SbWindowGetOnScreenKeyboardBoundingRect(SbWindow window,
+                                             SbWindowRect* bounding_rect) {
+  // Stub.
+  return true;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
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_hide_on_screen_keyboard.cc b/src/starboard/android/shared/window_hide_on_screen_keyboard.cc
new file mode 100644
index 0000000..4ecb6e8
--- /dev/null
+++ b/src/starboard/android/shared/window_hide_on_screen_keyboard.cc
@@ -0,0 +1,25 @@
+// 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/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowHideOnScreenKeyboard(SbWindow window, int ticket) {
+  starboard::android::shared::ApplicationAndroid::Get()
+      ->SbWindowHideOnScreenKeyboard(window, ticket);
+  return;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
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/shared/window_is_on_screen_keyboard_shown.cc b/src/starboard/android/shared/window_is_on_screen_keyboard_shown.cc
new file mode 100644
index 0000000..711f85c
--- /dev/null
+++ b/src/starboard/android/shared/window_is_on_screen_keyboard_shown.cc
@@ -0,0 +1,22 @@
+// 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/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+bool SbWindowIsOnScreenKeyboardShown(SbWindow window) {
+  // Stub.
+  return true;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc b/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc
new file mode 100644
index 0000000..fd38256
--- /dev/null
+++ b/src/starboard/android/shared/window_set_on_screen_keyboard_keep_focus.cc
@@ -0,0 +1,22 @@
+// 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/window.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowSetOnScreenKeyboardKeepFocus(SbWindow window, bool keep_focus) {
+  // Stub.
+  return;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)
diff --git a/src/starboard/android/shared/window_show_on_screen_keyboard.cc b/src/starboard/android/shared/window_show_on_screen_keyboard.cc
new file mode 100644
index 0000000..d5be7c8
--- /dev/null
+++ b/src/starboard/android/shared/window_show_on_screen_keyboard.cc
@@ -0,0 +1,27 @@
+// 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/window.h"
+
+#include "starboard/android/shared/application_android.h"
+
+#if SB_HAS(ON_SCREEN_KEYBOARD)
+void SbWindowShowOnScreenKeyboard(SbWindow window,
+                                  const char* input_text,
+                                  int ticket) {
+  starboard::android::shared::ApplicationAndroid::Get()
+      ->SbWindowShowOnScreenKeyboard(window, input_text, ticket);
+  return;
+}
+#endif  // SB_HAS(ON_SCREEN_KEYBOARD)