Import Cobalt 21.lts.2.289852
diff --git a/src/base/trace_event/trace_event_memory_overhead.h b/src/base/trace_event/trace_event_memory_overhead.h
index 2687e93..7016f0f 100644
--- a/src/base/trace_event/trace_event_memory_overhead.h
+++ b/src/base/trace_event/trace_event_memory_overhead.h
@@ -6,6 +6,7 @@
 #define BASE_TRACE_EVENT_TRACE_EVENT_MEMORY_OVERHEAD_H_
 
 #include <unordered_map>
+#include <string>
 
 #include "base/base_export.h"
 #include "base/macros.h"
diff --git a/src/cobalt/CHANGELOG.md b/src/cobalt/CHANGELOG.md
index f2872b4..644ecae 100644
--- a/src/cobalt/CHANGELOG.md
+++ b/src/cobalt/CHANGELOG.md
@@ -119,6 +119,12 @@
    the comment of `SbMediaCanPlayMimeAndKeySystem()` in `media.h` for more
    details.
 
+ - **Added support for controlling shutdown behavior of graphics system.**
+
+   Cobalt normally clears the framebuffer to opaque black on suspend or exit.
+   This behavior can now be overridden by implementing the cobalt extension
+   function `CobaltExtensionGraphicsApi::ShouldClearFrameOnShutdown`.
+
 ## Version 20
 
  - **Support for QUIC and SPDY is now enabled.**
diff --git a/src/cobalt/bindings/run_cobalt_bindings_tests.py b/src/cobalt/bindings/run_cobalt_bindings_tests.py
old mode 100644
new mode 100755
index a9e3be7..f852272
--- a/src/cobalt/bindings/run_cobalt_bindings_tests.py
+++ b/src/cobalt/bindings/run_cobalt_bindings_tests.py
@@ -27,6 +27,7 @@
 import sys
 
 import _env  # pylint: disable=unused-import
+
 from cobalt.bindings.idl_compiler_cobalt import IdlCompilerCobalt
 from cobalt.bindings.mozjs45.code_generator_mozjs45 import CodeGeneratorMozjs45
 from cobalt.bindings.v8c.code_generator_v8c import CodeGeneratorV8c
diff --git a/src/cobalt/bindings/testing/date_bindings_test.cc b/src/cobalt/bindings/testing/date_bindings_test.cc
index 602f2ee..bb58362 100644
--- a/src/cobalt/bindings/testing/date_bindings_test.cc
+++ b/src/cobalt/bindings/testing/date_bindings_test.cc
@@ -14,8 +14,11 @@
 
 #include "base/logging.h"
 
+#include "base/time/time.h"
 #include "cobalt/bindings/testing/bindings_test_base.h"
 #include "cobalt/bindings/testing/interface_with_date.h"
+#include "starboard/client_porting/eztime/eztime.h"
+#include "starboard/time_zone.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -79,6 +82,45 @@
   EXPECT_LT(std::abs(posix_now_ms - js_now_ms), 1000);
 }
 
+TEST_F(DateBindingsTest, StarboardTimeZone) {
+  const char* month_table[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+                               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+  std::string result;
+
+  EvaluateScript("new Date().toString();", &result);
+  base::Time now = base::Time::Now();
+
+  SB_LOG(INFO) << "JavaScript Date now is : " << result;
+  SB_LOG(INFO) << "and base::Time is: " << now;
+
+  base::Time::Exploded exploded;
+  now.LocalExplode(&exploded);
+  EXPECT_NE(result.find(std::to_string(exploded.year)), std::string::npos);
+  EXPECT_NE(result.find(month_table[exploded.month - 1]), std::string::npos);
+  EXPECT_NE(result.find(std::to_string(exploded.day_of_month)),
+            std::string::npos);
+  EXPECT_NE(result.find(std::to_string(exploded.hour)), std::string::npos);
+  EXPECT_NE(result.find(std::to_string(exploded.minute)), std::string::npos);
+  EXPECT_NE(result.find(std::to_string(exploded.second)), std::string::npos);
+}
+
+TEST_F(DateBindingsTest, TimezoneOffset) {
+  EzTimeExploded ez_exploded;
+
+  auto eztt = EzTimeTFromSbTime(SbTimeGetNow());
+  EzTimeTExplodeLocal(&eztt, &ez_exploded);
+  // ez_exploded is already local time, use UTC method to convert to
+  // EzTimeT.
+  EzTimeT local_time_minutes = EzTimeTImplodeUTC(&ez_exploded) / 60;
+  EzTimeT utc_minutes = EzTimeTFromSbTime(SbTimeGetNow()) / 60;
+  EzTimeT timezone_offset = utc_minutes - local_time_minutes;
+
+  std::string result;
+  EvaluateScript("new Date().getTimezoneOffset();", &result);
+
+  EXPECT_EQ(result, std::to_string(utc_minutes - local_time_minutes));
+}
+
 }  // namespace
 }  // namespace testing
 }  // namespace bindings
diff --git a/src/cobalt/browser/application.cc b/src/cobalt/browser/application.cc
index f5459ed..63e56d4 100644
--- a/src/cobalt/browser/application.cc
+++ b/src/cobalt/browser/application.cc
@@ -18,6 +18,7 @@
 
 #include "cobalt/browser/application.h"
 
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
@@ -255,6 +256,15 @@
   return initial_url;
 }
 
+bool ValidateSplashScreen(const base::Optional<GURL>& url) {
+  if (url->is_valid() &&
+      (url->SchemeIsFile() || url->SchemeIs("h5vcc-embedded"))) {
+    return true;
+  }
+  LOG(FATAL) << "Ignoring invalid fallback splash screen: " << url->spec();
+  return false;
+}
+
 base::Optional<GURL> GetFallbackSplashScreenURL() {
   base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
   std::string fallback_splash_screen_string;
@@ -270,15 +280,51 @@
   }
   base::Optional<GURL> fallback_splash_screen_url =
       GURL(fallback_splash_screen_string);
-  if (!fallback_splash_screen_url->is_valid() ||
-      !(fallback_splash_screen_url->SchemeIsFile() ||
-        fallback_splash_screen_url->SchemeIs("h5vcc-embedded"))) {
-    LOG(FATAL) << "Ignoring invalid fallback splash screen: "
-               << fallback_splash_screen_string;
-  }
+  ValidateSplashScreen(fallback_splash_screen_url);
   return fallback_splash_screen_url;
 }
 
+// Parses the fallback_splash_screen_topics command line parameter
+// and maps topics to full file url locations, if valid.
+void ParseFallbackSplashScreenTopics(
+    const base::Optional<GURL>& default_fallback_splash_screen_url,
+    std::map<std::string, GURL>* fallback_splash_screen_topic_map) {
+  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+  std::string topics;
+  if (command_line->HasSwitch(switches::kFallbackSplashScreenTopics)) {
+    topics = command_line->GetSwitchValueASCII(
+        switches::kFallbackSplashScreenTopics);
+  } else {
+    topics = configuration::Configuration::GetInstance()
+                 ->CobaltFallbackSplashScreenTopics();
+  }
+
+  // Note: values in topics_map may be either file paths or filenames.
+  std::map<std::string, std::string> topics_map;
+  BrowserModule::GetParamMap(topics, topics_map);
+  for (auto iterator = topics_map.begin(); iterator != topics_map.end();
+       iterator++) {
+    std::string topic = iterator->first;
+    std::string location = iterator->second;
+    base::Optional<GURL> topic_fallback_url = GURL(location);
+
+    // If not a valid url, check whether it is a valid filename in the
+    // same directory as the default fallback url.
+    if (!topic_fallback_url->is_valid()) {
+      if (default_fallback_splash_screen_url) {
+        topic_fallback_url = GURL(
+            default_fallback_splash_screen_url->GetWithoutFilename().spec() +
+            location);
+      } else {
+        break;
+      }
+    }
+    if (ValidateSplashScreen(topic_fallback_url)) {
+      (*fallback_splash_screen_topic_map)[topic] = topic_fallback_url.value();
+    }
+  }
+}
+
 base::TimeDelta GetTimedTraceDuration() {
 #if defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES)
   base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
@@ -579,6 +625,9 @@
   options.build_auto_mem_settings = memory_settings::GetDefaultBuildSettings();
   options.fallback_splash_screen_url = fallback_splash_screen_url;
 
+  ParseFallbackSplashScreenTopics(fallback_splash_screen_url,
+                                  &options.fallback_splash_screen_topic_map);
+
   if (command_line->HasSwitch(browser::switches::kFPSPrint)) {
     options.renderer_module_options.enable_fps_stdout = true;
   }
diff --git a/src/cobalt/browser/browser_module.cc b/src/cobalt/browser/browser_module.cc
index 383de3e..e143407 100644
--- a/src/cobalt/browser/browser_module.cc
+++ b/src/cobalt/browser/browser_module.cc
@@ -15,6 +15,7 @@
 #include "cobalt/browser/browser_module.h"
 
 #include <algorithm>
+#include <map>
 #include <memory>
 #include <vector>
 
@@ -31,6 +32,7 @@
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/cobalt_paths.h"
+#include "cobalt/base/init_cobalt.h"
 #include "cobalt/base/source_location.h"
 #include "cobalt/base/tokens.h"
 #include "cobalt/browser/on_screen_keyboard_starboard_bridge.h"
@@ -452,6 +454,7 @@
                    base::Unretained(this))));
   }
 
+  // Set the fallback splash screen url to the default fallback url.
   fallback_splash_screen_url_ = options.fallback_splash_screen_url;
 
   // Synchronously construct our WebModule object.
@@ -562,15 +565,17 @@
   DestroySplashScreen(base::TimeDelta());
   if (options_.enable_splash_screen_on_reloads ||
       main_web_module_generation_ == 1) {
-    base::Optional<std::string> key = SplashScreenCache::GetKeyForStartUrl(url);
+    base::Optional<std::string> topic = SetSplashScreenTopicFallback(url);
+    splash_screen_cache_->SetUrl(url, topic);
+
     if (fallback_splash_screen_url_ ||
-        (key && splash_screen_cache_->IsSplashScreenCached(*key))) {
+        splash_screen_cache_->IsSplashScreenCached()) {
       splash_screen_.reset(new SplashScreen(
           application_state_,
           base::Bind(&BrowserModule::QueueOnSplashScreenRenderTreeProduced,
                      base::Unretained(this)),
           network_module_, viewport_size, GetResourceProvider(),
-          kLayoutMaxRefreshFrequencyInHz, fallback_splash_screen_url_, url,
+          kLayoutMaxRefreshFrequencyInHz, fallback_splash_screen_url_,
           splash_screen_cache_.get(),
           base::Bind(&BrowserModule::DestroySplashScreen, weak_this_)));
       lifecycle_observers_.AddObserver(splash_screen_.get());
@@ -1893,5 +1898,64 @@
   return system_window_->GetSbWindow();
 }
 
+base::Optional<std::string> BrowserModule::SetSplashScreenTopicFallback(
+    const GURL& url) {
+  std::map<std::string, std::string> url_param_map;
+  // If this is the initial startup, use topic within deeplink, if specified.
+  if (main_web_module_generation_ == 1) {
+    GetParamMap(GetInitialDeepLink(), url_param_map);
+  }
+  // If this is not the initial startup, there was no deeplink specified, or
+  // the deeplink did not have a topic, check the current url for a topic.
+  if (url_param_map["topic"].empty()) {
+    GetParamMap(url.query(), url_param_map);
+  }
+  std::string splash_topic = url_param_map["topic"];
+  // If a topic was found, check whether a fallback url was specified.
+  if (!splash_topic.empty()) {
+    GURL splash_url = options_.fallback_splash_screen_topic_map[splash_topic];
+    if (!splash_url.spec().empty()) {
+      // Update fallback splash screen url to topic-specific URL.
+      fallback_splash_screen_url_ = splash_url;
+    }
+    return base::Optional<std::string>(splash_topic);
+  }
+  return base::Optional<std::string>();
+}
+
+void BrowserModule::GetParamMap(const std::string& url,
+                                std::map<std::string, std::string>& map) {
+  bool next_is_option = true;
+  bool next_is_value = false;
+  std::string option = "";
+  base::StringTokenizer tokenizer(url, "&=");
+  tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
+
+  while (tokenizer.GetNext()) {
+    if (tokenizer.token_is_delim()) {
+      switch (*tokenizer.token_begin()) {
+        case '&':
+          next_is_option = true;
+          break;
+        case '=':
+          next_is_value = true;
+          break;
+      }
+    } else {
+      std::string token = tokenizer.token();
+      if (next_is_value && !option.empty()) {
+        // Overwrite previous value when an option appears more than once.
+        map[option] = token;
+      }
+      option = "";
+      if (next_is_option) {
+        option = token;
+      }
+      next_is_option = false;
+      next_is_value = false;
+    }
+  }
+}
+
 }  // namespace browser
 }  // namespace cobalt
diff --git a/src/cobalt/browser/browser_module.h b/src/cobalt/browser/browser_module.h
index 15e0b94..f5bce8f 100644
--- a/src/cobalt/browser/browser_module.h
+++ b/src/cobalt/browser/browser_module.h
@@ -15,6 +15,7 @@
 #ifndef COBALT_BROWSER_BROWSER_MODULE_H_
 #define COBALT_BROWSER_BROWSER_MODULE_H_
 
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
@@ -104,6 +105,7 @@
     memory_settings::AutoMemSettings command_line_auto_mem_settings;
     memory_settings::AutoMemSettings build_auto_mem_settings;
     base::Optional<GURL> fallback_splash_screen_url;
+    std::map<std::string, GURL> fallback_splash_screen_topic_map;
     base::Optional<cssom::ViewportSize> requested_viewport_size;
     bool enable_splash_screen_on_reloads;
     bool enable_on_screen_keyboard = true;
@@ -215,6 +217,11 @@
 
   bool IsWebModuleLoaded() { return web_module_loaded_.IsSignaled(); }
 
+  // Parses url and defines a mapping of parameter values of the form
+  // &option=value&foo=bar.
+  static void GetParamMap(const std::string& url,
+                          std::map<std::string, std::string>& map);
+
  private:
 #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
   static void CoreDumpHandler(void* browser_module_as_void);
@@ -431,6 +438,10 @@
   // applied according to the current time.
   scoped_refptr<render_tree::Node> GetLastSubmissionAnimated();
 
+  // Sets the fallback splash screen url to a topic-specific URL, if applicable.
+  // Returns the topic used, or an empty Optional if a topic isn't found.
+  base::Optional<std::string> SetSplashScreenTopicFallback(const GURL& url);
+
   // TODO:
   //     WeakPtr usage here can be avoided if BrowserModule has a thread to
   //     own where it can ensure that its tasks are all resolved when it is
diff --git a/src/cobalt/browser/splash_screen.cc b/src/cobalt/browser/splash_screen.cc
index 962142f..5521bec 100644
--- a/src/cobalt/browser/splash_screen.cc
+++ b/src/cobalt/browser/splash_screen.cc
@@ -57,7 +57,6 @@
     const cssom::ViewportSize& window_dimensions,
     render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
     const base::Optional<GURL>& fallback_splash_screen_url,
-    const GURL& initial_main_web_module_url,
     SplashScreenCache* splash_screen_cache,
     const base::Callback<void(base::TimeDelta)>&
         on_splash_screen_shutdown_complete)
@@ -76,15 +75,10 @@
       base::ThreadPriority::HIGHEST;
 
   base::Optional<GURL> url_to_pass = fallback_splash_screen_url;
-  // Use the cached URL rather than the passed in URL if it exists.
-  base::Optional<std::string> key =
-      SplashScreenCache::GetKeyForStartUrl(initial_main_web_module_url);
   DCHECK(fallback_splash_screen_url ||
-         (key && splash_screen_cache &&
-          splash_screen_cache->IsSplashScreenCached(*key)));
-  if (key && splash_screen_cache &&
-      splash_screen_cache->IsSplashScreenCached(*key)) {
-    url_to_pass = GURL(loader::kCacheScheme + ("://" + *key));
+         (splash_screen_cache && splash_screen_cache->IsSplashScreenCached()));
+  if (splash_screen_cache && splash_screen_cache->IsSplashScreenCached()) {
+    url_to_pass = splash_screen_cache->GetCachedSplashScreenUrl();
     web_module_options.can_fetch_cache = true;
     web_module_options.splash_screen_cache = splash_screen_cache;
   }
diff --git a/src/cobalt/browser/splash_screen.h b/src/cobalt/browser/splash_screen.h
index 853c584..5cce8e8 100644
--- a/src/cobalt/browser/splash_screen.h
+++ b/src/cobalt/browser/splash_screen.h
@@ -42,7 +42,6 @@
                render_tree::ResourceProvider* resource_provider,
                float layout_refresh_rate,
                const base::Optional<GURL>& fallback_splash_screen_url,
-               const GURL& initial_main_web_module_url,
                cobalt::browser::SplashScreenCache* splash_screen_cache,
                const base::Callback<void(base::TimeDelta)>&
                    on_splash_screen_shutdown_complete);
diff --git a/src/cobalt/browser/splash_screen_cache.cc b/src/cobalt/browser/splash_screen_cache.cc
index 43cb6f7..7631ae3 100644
--- a/src/cobalt/browser/splash_screen_cache.cc
+++ b/src/cobalt/browser/splash_screen_cache.cc
@@ -18,6 +18,7 @@
 #include <string>
 #include <vector>
 
+#include "base/base64.h"
 #include "base/hash.h"
 #include "base/optional.h"
 #include "base/strings/string_util.h"
@@ -59,10 +60,15 @@
   base::AutoLock lock(lock_);
 }
 
-bool SplashScreenCache::CacheSplashScreen(const std::string& key,
-                                          const std::string& content) const {
+bool SplashScreenCache::CacheSplashScreen(
+    const std::string& content,
+    const base::Optional<std::string>& topic) const {
   base::AutoLock lock(lock_);
-  if (key.empty()) {
+  // Cache the content so that it's retrievable for the topic specified in the
+  // rel attribute. This topic may or may not match the topic-specified for this
+  // particular startup, tracked with "topic_".
+  base::Optional<std::string> key = GetKeyForStartConfig(url_, topic);
+  if (!key) {
     return false;
   }
 
@@ -76,10 +82,11 @@
                        kSbFileMaxPath)) {
     return false;
   }
-  if (!CreateDirsForKey(key)) {
+  if (!CreateDirsForKey(key.value())) {
     return false;
   }
-  std::string full_path = std::string(path.data()) + kSbFileSepString + key;
+  std::string full_path =
+      std::string(path.data()) + kSbFileSepString + key.value();
   starboard::ScopedFile cache_file(
       full_path.c_str(), kSbFileCreateAlways | kSbFileWrite, NULL, NULL);
 
@@ -87,15 +94,18 @@
                              static_cast<int>(content.size())) > 0;
 }
 
-bool SplashScreenCache::IsSplashScreenCached(const std::string& key) const {
+bool SplashScreenCache::IsSplashScreenCached() const {
   base::AutoLock lock(lock_);
   std::vector<char> path(kSbFileMaxPath, 0);
   if (!SbSystemGetPath(kSbSystemPathCacheDirectory, path.data(),
                        kSbFileMaxPath)) {
     return false;
   }
-  std::string full_path = std::string(path.data()) + kSbFileSepString + key;
-  return !key.empty() && SbFileExists(full_path.c_str());
+  base::Optional<std::string> key = GetKeyForStartConfig(url_, topic_);
+  if (!key) return false;
+  std::string full_path =
+      std::string(path.data()) + kSbFileSepString + key.value();
+  return SbFileExists(full_path.c_str());
 }
 
 int SplashScreenCache::ReadCachedSplashScreen(
@@ -126,9 +136,8 @@
   return result_size;
 }
 
-// static
-base::Optional<std::string> SplashScreenCache::GetKeyForStartUrl(
-    const GURL& url) {
+base::Optional<std::string> SplashScreenCache::GetKeyForStartConfig(
+    const GURL& url, const base::Optional<std::string>& topic) const {
   base::Optional<std::string> encoded_url = base::GetApplicationKey(url);
   if (!encoded_url) {
     return base::nullopt;
@@ -142,26 +151,36 @@
   }
 
   std::string subpath = "";
-  std::string subcomponent = kSbFileSepString + std::string("splash_screen");
-  if (SbStringConcat(path.data(), subcomponent.c_str(), kSbFileMaxPath) >=
-      static_cast<int>(kSbFileMaxPath)) {
+  if (!AddPathDirectory(std::string("splash_screen"), path, subpath)) {
     return base::nullopt;
   }
-  subpath += "splash_screen";
-  subcomponent = kSbFileSepString + *encoded_url;
-  if (SbStringConcat(path.data(), subcomponent.c_str(), kSbFileMaxPath) >=
-      static_cast<int>(kSbFileMaxPath)) {
+  if (!AddPathDirectory(*encoded_url, path, subpath)) {
     return base::nullopt;
   }
-  subpath += subcomponent;
-  subcomponent = kSbFileSepString + std::string("splash.html");
-  if (SbStringConcat(path.data(), subcomponent.c_str(), kSbFileMaxPath) >
-      static_cast<int>(kSbFileMaxPath)) {
+  if (topic && !topic.value().empty()) {
+    std::string encoded_topic;
+    base::Base64Encode(topic.value(), &encoded_topic);
+    if (!AddPathDirectory(encoded_topic, path, subpath)) {
+      return base::nullopt;
+    }
+  }
+  if (!AddPathDirectory(std::string("splash.html"), path, subpath)) {
     return base::nullopt;
   }
-  subpath += subcomponent;
 
-  return subpath;
+  return subpath.erase(0, 1);  // Remove leading separator
+}
+
+bool SplashScreenCache::AddPathDirectory(const std::string& directory,
+                                         std::vector<char>& path,
+                                         std::string& subpath) const {
+  std::string subcomponent = kSbFileSepString + directory;
+  if (SbStringConcat(path.data(), subcomponent.c_str(), kSbFileMaxPath) >=
+      static_cast<int>(kSbFileMaxPath)) {
+    return false;
+  }
+  subpath += subcomponent;
+  return true;
 }
 
 }  // namespace browser
diff --git a/src/cobalt/browser/splash_screen_cache.h b/src/cobalt/browser/splash_screen_cache.h
index deacfc8..b10e8cd 100644
--- a/src/cobalt/browser/splash_screen_cache.h
+++ b/src/cobalt/browser/splash_screen_cache.h
@@ -17,9 +17,11 @@
 
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "base/optional.h"
 #include "base/synchronization/lock.h"
+#include "cobalt/loader/cache_fetcher.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -36,25 +38,46 @@
   SplashScreenCache();
 
   // Cache the splash screen.
-  bool CacheSplashScreen(const std::string& key,
-                         const std::string& content) const;
+  bool CacheSplashScreen(const std::string& content,
+                         const base::Optional<std::string>& topic) const;
 
   // Read the cached the splash screen.
   int ReadCachedSplashScreen(const std::string& key,
                              std::unique_ptr<char[]>* result) const;
 
-  // Determine if a splash screen is cached corresponding to the key.
-  bool IsSplashScreenCached(const std::string& key) const;
+  // Determine if a splash screen is cached corresponding to the current url.
+  bool IsSplashScreenCached() const;
 
-  // Get the key that corresponds to a starting URL. Optionally create
-  // subdirectories along the path.
-  static base::Optional<std::string> GetKeyForStartUrl(const GURL& url);
+  // Set the URL of the currently requested splash screen.
+  void SetUrl(const GURL& url, const base::Optional<std::string>& topic) {
+    url_ = url;
+    topic_ = topic;
+  }
+
+  // Get the cache location of the currently requested splash screen.
+  GURL GetCachedSplashScreenUrl() {
+    base::Optional<std::string> key = GetKeyForStartConfig(url_, topic_);
+    return GURL(loader::kCacheScheme + ("://" + *key));
+  }
 
  private:
+  // Get the key that corresponds to the starting URL and (optional) topic.
+  base::Optional<std::string> GetKeyForStartConfig(
+      const GURL& url, const base::Optional<std::string>& topic) const;
+
+  // Adds the directory to the path and subpath if the new path does not exceed
+  // maximum length. Returns true if successful.
+  bool AddPathDirectory(const std::string& directory, std::vector<char>& path,
+                        std::string& subpath) const;
+
   // Lock to protect access to the cache file.
   mutable base::Lock lock_;
   // Hash of the last read page contents.
   mutable uint32_t last_page_hash_;
+  // Latest url that was navigated to.
+  GURL url_;
+  // Splash topic associated with startup.
+  base::Optional<std::string> topic_;
 };
 
 }  // namespace browser
diff --git a/src/cobalt/browser/switches.cc b/src/cobalt/browser/switches.cc
index 915b762..64653d5 100644
--- a/src/cobalt/browser/switches.cc
+++ b/src/cobalt/browser/switches.cc
@@ -390,6 +390,17 @@
     "no value is set, the URL in gyp_configuration.gypi or base.gypi will be "
     "used.";
 
+const char kFallbackSplashScreenTopics[] = "fallback_splash_screen_topics";
+const char kFallbackSplashScreenTopicsHelp[] =
+    "Setting this switch defines a mapping of URL 'topics' to splash screen "
+    "URLs or filenames that Cobalt will use in the absence of a web cache, "
+    "(for example, music=music_splash_screen.html&foo=file:///bar.html). If a "
+    "URL is given it should match the format of 'fallback_splash_screen_url'. "
+    "A given filename should exist in the same directory as "
+    "'fallback_splash_screen_url'. If no fallback url exists for the topic of "
+    "the URL used to launch Cobalt, then the value of "
+    "'fallback_splash_screen_url' will be used.";
+
 const char kVersion[] = "version";
 const char kVersionHelp[] = "Prints the current version of Cobalt";
 
diff --git a/src/cobalt/browser/switches.h b/src/cobalt/browser/switches.h
index f70c0f8..67c03f7 100644
--- a/src/cobalt/browser/switches.h
+++ b/src/cobalt/browser/switches.h
@@ -151,6 +151,8 @@
 extern const char kSoftwareSurfaceCacheSizeInBytesHelp[];
 extern const char kFallbackSplashScreenURL[];
 extern const char kFallbackSplashScreenURLHelp[];
+extern const char kFallbackSplashScreenTopics[];
+extern const char kFallbackSplashScreenTopicsHelp[];
 extern const char kVersion[];
 extern const char kVersionHelp[];
 extern const char kViewport[];
diff --git a/src/cobalt/browser/web_module.cc b/src/cobalt/browser/web_module.cc
index 1923513..7d0a353 100644
--- a/src/cobalt/browser/web_module.cc
+++ b/src/cobalt/browser/web_module.cc
@@ -86,23 +86,20 @@
 // deeper than this could be discarded, and will not be rendered.
 const int kDOMMaxElementDepth = 32;
 
-bool CacheUrlContent(SplashScreenCache* splash_screen_cache, const GURL& url,
-                     const std::string& content) {
-  base::Optional<std::string> key = SplashScreenCache::GetKeyForStartUrl(url);
-  if (key) {
-    return splash_screen_cache->SplashScreenCache::CacheSplashScreen(*key,
-                                                                     content);
-  }
-  return false;
+void CacheUrlContent(SplashScreenCache* splash_screen_cache,
+                     const std::string& content,
+                     const base::Optional<std::string>& topic) {
+  splash_screen_cache->SplashScreenCache::CacheSplashScreen(content, topic);
 }
 
-base::Callback<bool(const GURL&, const std::string&)> CacheUrlContentCallback(
-    SplashScreenCache* splash_screen_cache) {
+base::Callback<void(const std::string&, const base::Optional<std::string>&)>
+CacheUrlContentCallback(SplashScreenCache* splash_screen_cache) {
   // This callback takes in first the url, then the content string.
   if (splash_screen_cache) {
     return base::Bind(CacheUrlContent, base::Unretained(splash_screen_cache));
   } else {
-    return base::Callback<bool(const GURL&, const std::string&)>();
+    return base::Callback<void(const std::string&,
+                               const base::Optional<std::string>&)>();
   }
 }
 
diff --git a/src/cobalt/build/build.id b/src/cobalt/build/build.id
index f24700a..e9bfa85 100644
--- a/src/cobalt/build/build.id
+++ b/src/cobalt/build/build.id
@@ -1 +1 @@
-283720
\ No newline at end of file
+289852
\ No newline at end of file
diff --git a/src/cobalt/build/cobalt_configuration.py b/src/cobalt/build/cobalt_configuration.py
index 691c283..4815b88 100644
--- a/src/cobalt/build/cobalt_configuration.py
+++ b/src/cobalt/build/cobalt_configuration.py
@@ -38,8 +38,17 @@
                          application_directory)
 
   def GetVariables(self, config_name):
+
+    # Use env var to optimize build speed on CI
+    try:
+      # Force to int, so it's easy to pass in an override.
+      use_fastbuild = int(os.environ.get('IS_CI', 0))
+    except (ValueError, TypeError):
+      use_fastbuild = 0
+
     variables = {
-        'cobalt_fastbuild': os.environ.get('LB_FASTBUILD', 0),
+        # This is used to omit large debuginfo in files on CI environment
+        'cobalt_fastbuild': use_fastbuild,
 
         # This is here rather than cobalt_configuration.gypi so that it's
         # available for browser_bindings_gen.gyp.
@@ -114,7 +123,15 @@
 
         # XMLHttpRequest: send() - Redirects (basics) (307).
         # Disabled because of: Flaky.
-        'xhr/WebPlatformTest.Run/XMLHttpRequest_send_redirect_htm'
+        'xhr/WebPlatformTest.Run/XMLHttpRequest_send_redirect_htm',
+
+        # Disabled because of: Flaky on buildbot across multiple buildconfigs.
+        # Non-reproducible with local runs.
+        ('xhr/WebPlatformTest.Run/'
+         'XMLHttpRequest_send_entity_body_get_head_async_htm'),
+        'xhr/WebPlatformTest.Run/XMLHttpRequest_status_error_htm',
+        'xhr/WebPlatformTest.Run/XMLHttpRequest_response_json_htm',
+        'xhr/WebPlatformTest.Run/XMLHttpRequest_send_redirect_to_non_cors_htm',
     ]
     return filters
 
diff --git a/src/cobalt/build/gyp_utils.py b/src/cobalt/build/gyp_utils.py
index cabcbe1..986d81c 100644
--- a/src/cobalt/build/gyp_utils.py
+++ b/src/cobalt/build/gyp_utils.py
@@ -25,7 +25,7 @@
 import _env  # pylint: disable=unused-import
 from cobalt.tools import paths
 
-
+_REVINFO_KEY = 'cobalt_src'
 _VERSION_SERVER_URL = 'https://carbon-airlock-95823.appspot.com/build_version/generate'  # pylint:disable=line-too-long
 _XSSI_PREFIX = ")]}'\n"
 
@@ -34,32 +34,29 @@
 
 
 def GetRevinfo():
-  """Get absolute state of all git repos from gclient DEPS."""
+  """Get absolute state of all git repos."""
+
+  git_get_remote_args = ['config', '--get', 'remote.origin.url']
+  git_get_revision_args = ['rev-parse', 'HEAD']
 
   try:
-    revinfo_cmd = ['gclient', 'revinfo', '-a']
-
-    if sys.platform.startswith('linux') or sys.platform == 'darwin':
-      use_shell = False
-    else:
-      # Windows needs shell to find gclient in the PATH.
-      use_shell = True
-    output = subprocess.check_output(revinfo_cmd, shell=use_shell)
-    revinfo = {}
-    lines = output.splitlines()
-    for line in lines:
-      repo, url = line.split(':', 1)
-      repo = repo.strip().replace('\\', '/')
-      url = url.strip()
-      revinfo[repo] = url
-    return revinfo
-  except (subprocess.CalledProcessError, ValueError) as e:
-    logging.warning('Failed to get revision information: %s', e)
+    cobalt_remote = subprocess.check_output(['git'] +
+                                            git_get_remote_args).strip()
+    cobalt_rev = subprocess.check_output(['git'] +
+                                         git_get_revision_args).strip()
+    return {_REVINFO_KEY: '{}@{}'.format(cobalt_remote, cobalt_rev)}
+  except subprocess.CalledProcessError:
+    logging.info(
+        'Failed to get revision information. Trying again in src/ directory...')
     try:
-      logging.warning('Command output was: %s', line)
-    except NameError:
-      pass
-    return {}
+      cobalt_remote = subprocess.check_output(['git', '-C', 'src'] +
+                                              git_get_remote_args).strip()
+      cobalt_rev = subprocess.check_output(['git', '-C', 'src'] +
+                                           git_get_revision_args).strip()
+      return {_REVINFO_KEY: '{}@{}'.format(cobalt_remote, cobalt_rev)}
+    except subprocess.CalledProcessError as e:
+      logging.warning('Failed to get revision information: %s', e)
+      return {}
 
 
 def GetBuildNumber(version_server=_VERSION_SERVER_URL):
@@ -119,10 +116,10 @@
     match = search_re.search(f.read())
 
   if not match:
-    logging.critical('Could not query constant value.  The expression '
-                     'should only have numbers, operators, spaces, and '
-                     'parens.  Please check "%s" in %s.\n', constant_name,
-                     file_path)
+    logging.critical(
+        'Could not query constant value.  The expression '
+        'should only have numbers, operators, spaces, and '
+        'parens.  Please check "%s" in %s.\n', constant_name, file_path)
     sys.exit(1)
 
   expression = match.group(1)
diff --git a/src/cobalt/configuration/configuration.cc b/src/cobalt/configuration/configuration.cc
index 9c5087c..a5e9aad 100644
--- a/src/cobalt/configuration/configuration.cc
+++ b/src/cobalt/configuration/configuration.cc
@@ -155,6 +155,13 @@
 #endif
 }
 
+const char* Configuration::CobaltFallbackSplashScreenTopics() {
+  if (configuration_api_ && configuration_api_->version >= 2) {
+    return configuration_api_->CobaltFallbackSplashScreenTopics();
+  }
+  return "";
+}
+
 bool Configuration::CobaltEnableQuic() {
   if (configuration_api_) {
 #if defined(COBALT_ENABLE_QUIC)
diff --git a/src/cobalt/configuration/configuration.h b/src/cobalt/configuration/configuration.h
index 6ea8ba5..6913ea1 100644
--- a/src/cobalt/configuration/configuration.h
+++ b/src/cobalt/configuration/configuration.h
@@ -59,6 +59,7 @@
   bool CobaltGcZeal();
   const char* CobaltRasterizerType();
   bool CobaltEnableJit();
+  const char* CobaltFallbackSplashScreenTopics();
 
  private:
   Configuration();
diff --git a/src/cobalt/doc/splash_screen.md b/src/cobalt/doc/splash_screen.md
index 9122555..fbb6b6d 100644
--- a/src/cobalt/doc/splash_screen.md
+++ b/src/cobalt/doc/splash_screen.md
@@ -49,15 +49,15 @@
      first time.
 
   3. **Build-time fallback splash screen:** If a web cached splash screen is
-     unavailable and command line parameters are not passed by the system, a
-     `gyp_configuration.gypi` fallback splash screen may be used. Porters should
-     set the gypi variable `fallback_splash_screen_url` to the splash screen
-     URL.
+     unavailable and command line parameters are not passed by the system,
+     a CobaltExtensionConfigurationApi fallback splash screen may be used.
+     Porters should set the `CobaltFallbackSplashScreenUrl` value in
+     `configuration.cc` to the splash screen URL.
 
-  4. **Default splash screen:** If no web cached splash screen is
-     available, and command line and `gyp_configuration.gypi` fallbacks are not
-     set, a default splash screen will be used. This is set in `base.gypi` via
-     `fallback_splash_screen_url%` to refer to a black splash screen.
+  4. **Default splash screen:** If no web cached splash screen is available, and
+     command line and CobaltExtensionConfigurationApi fallbacks are not set, a
+     default splash screen will be used. This is set in
+     `configuration_defaults.cc` to refer to a black splash screen.
 
 ## Web-updatability
 
@@ -85,6 +85,40 @@
 Cobalt will also need to read the cached splash screen from the cache directory
 when starting up.
 
+## Topic-specific splash screens
+
+It is possible to specify multiple splash screens for a given Cobalt-based
+application, using a start-up 'topic' to select between the available splash
+screens. This can be useful when an application has multiple entry points that
+require different splash screens. The topic may be specified in the start-up url
+or deeplink as a query parameter. For example,
+`https://www.example.com/path?topic=foo`. If a splash-screen has been specified
+for topic 'foo', it will be used. Otherwise, the topic is ignored. Topic values
+should be URL encoded and limited to alphanumeric characters, hyphens,
+underscores, and percent signs.
+
+There are three ways to specify topic-specific splash screens. These methods mirror
+the types of splash screens listed above, and unless specified, the rules here
+are the same as for non-topic-based splash screens.
+
+  1. **Web cached splash screen:** A custom `rel="<topic>_splashscreen"`
+     attribute on a link element is used to specify a topic-specific splash
+     screen. There can be any number of these elements with different topics, in
+     addition to the topic-neutral `rel="splashscreen"`.
+
+  2. **Command line fallback splash screen:** The command line argument
+     `--fallback_splash_screen_topics` can be used if the cache is unavailable.
+     The argument accepts a list of topic/file parameters. If a file is not a
+     valid URL path, then it will be used as a filename at the path specified by
+     `--fallback_splash_screen_url`. For example,
+     `foo_topic=file:///foo.html&bar=bar.html`.
+
+  3. **Build-time fallback splash screen:** If a web cached splash screen is
+     unavailable and command line parameters are not passed by the system, a
+     CobaltExtensionConfigurationApi fallback splash screen may be used. Porters
+     should set the `CobaltFallbackSplashScreenTopics` value in
+     `configuration.cc` and this value should look like the command line option.
+
 ## Application-specific splash screens
 
 On systems that plan to support multiple Cobalt-based applications, an
@@ -94,9 +128,8 @@
 the Cobalt binary must be handled by the system.
 
 Alternatively, an application developer may use the default black splash screen
-specified in base.gypi whenever a cached splash screen is not available and rely
-on the web application to specify an application-specific cached splash screen
-otherwise.
+whenever a cached splash screen is not available and rely on the web application
+to specify an application-specific cached splash screen otherwise.
 
 ## Provided embedded resource splash screens
 For convenience, we currently provide the following splash screens as embedded
diff --git a/src/cobalt/dom/dom_test.gyp b/src/cobalt/dom/dom_test.gyp
index f0b181f..a2a464e 100644
--- a/src/cobalt/dom/dom_test.gyp
+++ b/src/cobalt/dom/dom_test.gyp
@@ -44,6 +44,7 @@
         'font_cache_test.cc',
         'html_element_factory_test.cc',
         'html_element_test.cc',
+        'html_link_element_test.cc',
         'intersection_observer_test.cc',
         'keyboard_event_test.cc',
         'local_storage_database_test.cc',
diff --git a/src/cobalt/dom/html_link_element.cc b/src/cobalt/dom/html_link_element.cc
index 8244909..b7150de 100644
--- a/src/cobalt/dom/html_link_element.cc
+++ b/src/cobalt/dom/html_link_element.cc
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "base/bind.h"
+#include "base/strings/string_tokenizer.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/cssom/css_parser.h"
 #include "cobalt/cssom/css_style_sheet.h"
@@ -34,10 +35,34 @@
 namespace dom {
 namespace {
 
+bool IsValidRelChar(char const& c) {
+  return (isalnum(c) || c == '_' || c == '\\' || c == '-');
+}
+
+bool IsValidSplashScreenFormat(const std::string& rel) {
+  base::StringTokenizer tokenizer(rel, "_");
+  tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
+  bool is_valid_format = true;
+  while (tokenizer.GetNext()) {
+    std::string token = tokenizer.token();
+    if (SbStringCompareAll(token.c_str(), "splashscreen") == 0) {
+      is_valid_format = true;
+    } else {
+      for (char const& c : token) {
+        if (!IsValidRelChar(c)) {
+          return false;
+        }
+      }
+      is_valid_format = false;
+    }
+  }
+  return is_valid_format;
+}
+
 CspDelegate::ResourceType GetCspResourceTypeForRel(const std::string& rel) {
   if (rel == "stylesheet") {
     return CspDelegate::kStyle;
-  } else if (rel == "splashscreen") {
+  } else if (IsValidSplashScreenFormat(rel)) {
     return CspDelegate::kLocation;
   } else {
     NOTIMPLEMENTED();
@@ -71,13 +96,15 @@
 const char HTMLLinkElement::kTagName[] = "link";
 // static
 const std::vector<std::string> HTMLLinkElement::kSupportedRelValues = {
-    "stylesheet", "splashscreen"};
+    "stylesheet"};
 
 void HTMLLinkElement::OnInsertedIntoDocument() {
   HTMLElement::OnInsertedIntoDocument();
   if (std::find(kSupportedRelValues.begin(), kSupportedRelValues.end(),
                 rel()) != kSupportedRelValues.end()) {
     Obtain();
+  } else if (IsValidSplashScreenFormat(rel())) {
+    Obtain();
   } else {
     LOG(WARNING) << "<link> has unsupported rel value: " << rel() << ".";
   }
@@ -202,7 +229,7 @@
   Document* document = node_document();
   if (rel() == "stylesheet") {
     OnStylesheetLoaded(document, *content);
-  } else if (rel() == "splashscreen") {
+  } else if (IsValidSplashScreenFormat(rel())) {
     OnSplashscreenLoaded(document, *content);
   } else {
     NOTIMPLEMENTED();
@@ -257,7 +284,13 @@
 void HTMLLinkElement::OnSplashscreenLoaded(Document* document,
                                            const std::string& content) {
   scoped_refptr<Window> window = document->window();
-  window->CacheSplashScreen(content);
+  std::string link = rel();
+  size_t last_underscore = link.find_last_of("_");
+  base::Optional<std::string> topic;
+  if (last_underscore != std::string::npos) {
+    topic = link.substr(0, last_underscore);
+  }
+  window->CacheSplashScreen(content, topic);
 }
 
 void HTMLLinkElement::OnStylesheetLoaded(Document* document,
diff --git a/src/cobalt/dom/html_link_element.h b/src/cobalt/dom/html_link_element.h
index ba4ee01..e38abea 100644
--- a/src/cobalt/dom/html_link_element.h
+++ b/src/cobalt/dom/html_link_element.h
@@ -67,13 +67,14 @@
 
   DEFINE_WRAPPABLE_TYPE(HTMLLinkElement);
 
- private:
+ protected:
   ~HTMLLinkElement() override {}
 
+ private:
   void ResolveAndSetAbsoluteURL();
 
   // From the spec: HTMLLinkElement.
-  void Obtain();
+  virtual void Obtain();
 
   void OnContentProduced(const loader::Origin& last_url_origin,
                          std::unique_ptr<std::string> content);
diff --git a/src/cobalt/dom/html_link_element_test.cc b/src/cobalt/dom/html_link_element_test.cc
new file mode 100644
index 0000000..380711e
--- /dev/null
+++ b/src/cobalt/dom/html_link_element_test.cc
@@ -0,0 +1,114 @@
+// Copyright 2020 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/dom/html_link_element.h"
+#include "base/message_loop/message_loop.h"
+#include "cobalt/cssom/testing/mock_css_parser.h"
+#include "cobalt/dom/document.h"
+#include "cobalt/dom/dom_stat_tracker.h"
+#include "cobalt/dom/testing/stub_environment_settings.h"
+#include "cobalt/dom/window.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::NiceMock;
+
+namespace cobalt {
+namespace dom {
+
+class HTMLLinkElementMock : public HTMLLinkElement {
+ public:
+  explicit HTMLLinkElementMock(Document* document)
+      : HTMLLinkElement(document) {}
+  void Obtain() { obtained_ = true; }
+  bool obtained_ = false;
+  bool obtained() { return obtained_; }
+};
+
+class DocumentMock : public Document {
+ public:
+  explicit DocumentMock(HTMLElementContext* context) : Document(context) {}
+  scoped_refptr<HTMLLinkElementMock> CreateElement(
+      const std::string& local_name) {
+    return scoped_refptr<HTMLLinkElementMock>(new HTMLLinkElementMock(this));
+  }
+};
+
+class HtmlLinkElementTest : public ::testing::Test {
+ protected:
+  HtmlLinkElementTest()
+      : dom_stat_tracker_(new DomStatTracker("HtmlLinkElementTest")),
+        html_element_context_(&environment_settings_, NULL, NULL, &css_parser_,
+                              NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+                              NULL, NULL, NULL, NULL, dom_stat_tracker_.get(),
+                              "", base::kApplicationStateStarted, NULL),
+        document_(new DocumentMock(&html_element_context_)),
+        message_loop_(base::MessageLoop::TYPE_DEFAULT) {}
+
+  scoped_refptr<DocumentMock> document() { return document_; }
+  scoped_refptr<HTMLLinkElementMock> CreateDocumentWithLinkElement(
+      std::string rel = "");
+
+ private:
+  std::unique_ptr<DomStatTracker> dom_stat_tracker_;
+  testing::StubEnvironmentSettings environment_settings_;
+  NiceMock<cssom::testing::MockCSSParser> css_parser_;
+  HTMLElementContext html_element_context_;
+  scoped_refptr<DocumentMock> document_;
+  base::MessageLoop message_loop_;
+};
+
+scoped_refptr<HTMLLinkElementMock>
+HtmlLinkElementTest::CreateDocumentWithLinkElement(std::string rel) {
+  scoped_refptr<HTMLLinkElementMock> element_ =
+      document_->CreateElement("link");
+  if (!rel.empty()) {
+    element_->SetAttribute("rel", rel);
+  }
+  document_->AppendChild(element_);
+  return element_;
+}
+
+TEST_F(HtmlLinkElementTest, StylesheetRelAttribute) {
+  scoped_refptr<HTMLLinkElementMock> el =
+      CreateDocumentWithLinkElement("stylesheet");
+  EXPECT_TRUE(el->obtained());
+}
+
+TEST_F(HtmlLinkElementTest, SplashScreenRelAttribute) {
+  scoped_refptr<HTMLLinkElementMock> el =
+      CreateDocumentWithLinkElement("splashscreen");
+  EXPECT_TRUE(el->obtained());
+
+  el = CreateDocumentWithLinkElement("music_splashscreen");
+  EXPECT_TRUE(el->obtained());
+
+  el = CreateDocumentWithLinkElement("music-_\\2_splashscreen");
+  EXPECT_TRUE(el->obtained());
+}
+
+TEST_F(HtmlLinkElementTest, BadSplashScreenRelAttribute) {
+  scoped_refptr<HTMLLinkElementMock> el =
+      CreateDocumentWithLinkElement("bad*_splashscreen");
+  EXPECT_FALSE(el->obtained());
+
+  el = CreateDocumentWithLinkElement("badsplashscreen");
+  EXPECT_FALSE(el->obtained());
+
+  el = CreateDocumentWithLinkElement("splashscreen_bad");
+  EXPECT_FALSE(el->obtained());
+}
+
+}  // namespace dom
+}  // namespace cobalt
diff --git a/src/cobalt/dom/window.cc b/src/cobalt/dom/window.cc
index eac877d..989d187 100644
--- a/src/cobalt/dom/window.cc
+++ b/src/cobalt/dom/window.cc
@@ -702,12 +702,13 @@
   tracer->Trace(on_screen_keyboard_);
 }
 
-void Window::CacheSplashScreen(const std::string& content) {
+void Window::CacheSplashScreen(const std::string& content,
+                               const base::Optional<std::string>& topic) {
   if (splash_screen_cache_callback_.is_null()) {
     return;
   }
   DLOG(INFO) << "Caching splash screen for URL " << location()->url();
-  splash_screen_cache_callback_.Run(location()->url(), content);
+  splash_screen_cache_callback_.Run(content, topic);
 }
 
 const scoped_refptr<OnScreenKeyboard>& Window::on_screen_keyboard() const {
diff --git a/src/cobalt/dom/window.h b/src/cobalt/dom/window.h
index 6fbfc81..e7320b4 100644
--- a/src/cobalt/dom/window.h
+++ b/src/cobalt/dom/window.h
@@ -122,7 +122,9 @@
   // close() was called.
   typedef base::Callback<void(base::TimeDelta)> CloseCallback;
   typedef UrlRegistry<MediaSource> MediaSourceRegistry;
-  typedef base::Callback<bool(const GURL&, const std::string&)> CacheCallback;
+  typedef base::Callback<void(const std::string&,
+                              const base::Optional<std::string>&)>
+      CacheCallback;
 
   enum ClockType {
     kClockTypeTestRunner,
@@ -379,7 +381,8 @@
   void OnDocumentRootElementUnableToProvideOffsetDimensions();
 
   // Cache the passed in splash screen content for the window.location URL.
-  void CacheSplashScreen(const std::string& content);
+  void CacheSplashScreen(const std::string& content,
+                         const base::Optional<std::string>& topic);
 
   const scoped_refptr<loader::CORSPreflightCache> get_preflight_cache() {
     return preflight_cache_;
diff --git a/src/cobalt/extension/configuration.h b/src/cobalt/extension/configuration.h
index 9d379c7..7c12817 100644
--- a/src/cobalt/extension/configuration.h
+++ b/src/cobalt/extension/configuration.h
@@ -219,6 +219,11 @@
   // See "cobalt/doc/performance_tuning.md" for more information on when this
   // should be used.
   bool (*CobaltEnableJit)();
+
+  // The fields below this point were added in version 2 or later.
+
+  // A mapping of splash screen topics to fallback URLs.
+  const char* (*CobaltFallbackSplashScreenTopics)();
 } CobaltExtensionConfigurationApi;
 
 #ifdef __cplusplus
diff --git a/src/cobalt/extension/extension_test.cc b/src/cobalt/extension/extension_test.cc
index 3756cba..9f4caf0 100644
--- a/src/cobalt/extension/extension_test.cc
+++ b/src/cobalt/extension/extension_test.cc
@@ -29,23 +29,22 @@
   typedef CobaltExtensionPlatformServiceApi ExtensionApi;
   const char* kExtensionName = kCobaltExtensionPlatformServiceName;
 
-  const ExtensionApi* extension_api = static_cast<const ExtensionApi*>(
-      SbSystemGetExtension(kExtensionName));
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
   if (!extension_api) {
     return;
   }
 
   EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_TRUE(extension_api->version == 1 ||
-              extension_api->version == 2 ||
-              extension_api->version == 3) << "Invalid version";
-  EXPECT_TRUE(extension_api->Has != NULL);
-  EXPECT_TRUE(extension_api->Open != NULL);
-  EXPECT_TRUE(extension_api->Close != NULL);
-  EXPECT_TRUE(extension_api->Send != NULL);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 3u);
+  EXPECT_NE(extension_api->Has, nullptr);
+  EXPECT_NE(extension_api->Open, nullptr);
+  EXPECT_NE(extension_api->Close, nullptr);
+  EXPECT_NE(extension_api->Send, nullptr);
 
-  const ExtensionApi* second_extension_api = static_cast<const ExtensionApi*>(
-      SbSystemGetExtension(kExtensionName));
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
   EXPECT_EQ(second_extension_api, extension_api)
       << "Extension struct should be a singleton";
 }
@@ -54,35 +53,50 @@
   typedef CobaltExtensionGraphicsApi ExtensionApi;
   const char* kExtensionName = kCobaltExtensionGraphicsName;
 
-  const ExtensionApi* extension_api = static_cast<const ExtensionApi*>(
-      SbSystemGetExtension(kExtensionName));
+  const ExtensionApi* extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
   if (!extension_api) {
     return;
   }
 
   EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_TRUE(extension_api->version == 1 ||
-              extension_api->version == 2 ||
-              extension_api->version == 3) << "Invalid version";
-  EXPECT_TRUE(extension_api->GetMaximumFrameIntervalInMilliseconds != NULL);
-  if (extension_api->version >= 2) {
-    EXPECT_TRUE(extension_api->GetMinimumFrameIntervalInMilliseconds != NULL);
-  }
-  if (extension_api->version >= 3) {
-    EXPECT_TRUE(extension_api->IsMapToMeshEnabled != NULL);
-  }
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 4u);
 
+  EXPECT_NE(extension_api->GetMaximumFrameIntervalInMilliseconds, nullptr);
   float maximum_frame_interval =
       extension_api->GetMaximumFrameIntervalInMilliseconds();
   EXPECT_FALSE(std::isnan(maximum_frame_interval));
 
   if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->GetMinimumFrameIntervalInMilliseconds, nullptr);
     float minimum_frame_interval =
-      extension_api->GetMinimumFrameIntervalInMilliseconds();
+        extension_api->GetMinimumFrameIntervalInMilliseconds();
     EXPECT_GT(minimum_frame_interval, 0);
   }
-  const ExtensionApi* second_extension_api = static_cast<const ExtensionApi*>(
-      SbSystemGetExtension(kExtensionName));
+
+  if (extension_api->version >= 3) {
+    EXPECT_NE(extension_api->IsMapToMeshEnabled, nullptr);
+  }
+
+  if (extension_api->version >= 4) {
+    EXPECT_NE(extension_api->ShouldClearFrameOnShutdown, nullptr);
+    float clear_color_r, clear_color_g, clear_color_b, clear_color_a;
+    if (extension_api->ShouldClearFrameOnShutdown(
+            &clear_color_r, &clear_color_g, &clear_color_b, &clear_color_a)) {
+      EXPECT_GE(clear_color_r, 0.0f);
+      EXPECT_LE(clear_color_r, 1.0f);
+      EXPECT_GE(clear_color_g, 0.0f);
+      EXPECT_LE(clear_color_g, 1.0f);
+      EXPECT_GE(clear_color_b, 0.0f);
+      EXPECT_LE(clear_color_b, 1.0f);
+      EXPECT_GE(clear_color_a, 0.0f);
+      EXPECT_LE(clear_color_a, 1.0f);
+    }
+  }
+
+  const ExtensionApi* second_extension_api =
+      static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
   EXPECT_EQ(second_extension_api, extension_api)
       << "Extension struct should be a singleton";
 }
@@ -98,18 +112,17 @@
   }
 
   EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_TRUE(extension_api->version == 1 ||
-              extension_api->version == 2 ||
-              extension_api->version == 3) << "Invalid version";
-  EXPECT_TRUE(extension_api->GetCurrentInstallationIndex != NULL);
-  EXPECT_TRUE(extension_api->MarkInstallationSuccessful != NULL);
-  EXPECT_TRUE(extension_api->RequestRollForwardToInstallation != NULL);
-  EXPECT_TRUE(extension_api->GetInstallationPath != NULL);
-  EXPECT_TRUE(extension_api->SelectNewInstallationIndex != NULL);
-  EXPECT_TRUE(extension_api->GetAppKey != NULL);
-  EXPECT_TRUE(extension_api->GetMaxNumberInstallations != NULL);
-  EXPECT_TRUE(extension_api->ResetInstallation != NULL);
-  EXPECT_TRUE(extension_api->Reset != NULL);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 3u);
+  EXPECT_NE(extension_api->GetCurrentInstallationIndex, nullptr);
+  EXPECT_NE(extension_api->MarkInstallationSuccessful, nullptr);
+  EXPECT_NE(extension_api->RequestRollForwardToInstallation, nullptr);
+  EXPECT_NE(extension_api->GetInstallationPath, nullptr);
+  EXPECT_NE(extension_api->SelectNewInstallationIndex, nullptr);
+  EXPECT_NE(extension_api->GetAppKey, nullptr);
+  EXPECT_NE(extension_api->GetMaxNumberInstallations, nullptr);
+  EXPECT_NE(extension_api->ResetInstallation, nullptr);
+  EXPECT_NE(extension_api->Reset, nullptr);
   const ExtensionApi* second_extension_api =
       static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
   EXPECT_EQ(second_extension_api, extension_api)
@@ -127,27 +140,32 @@
   }
 
   EXPECT_STREQ(extension_api->name, kExtensionName);
-  EXPECT_TRUE(extension_api->version == 1);
-  EXPECT_TRUE(extension_api->CobaltUserOnExitStrategy != NULL);
-  EXPECT_TRUE(extension_api->CobaltRenderDirtyRegionOnly != NULL);
-  EXPECT_TRUE(extension_api->CobaltEglSwapInterval != NULL);
-  EXPECT_TRUE(extension_api->CobaltFallbackSplashScreenUrl != NULL);
-  EXPECT_TRUE(extension_api->CobaltEnableQuic != NULL);
-  EXPECT_TRUE(extension_api->CobaltSkiaCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltOffscreenTargetCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltEncodedImageCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltImageCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltLocalTypefaceCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltRemoteTypefaceCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltMeshCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltSoftwareSurfaceCacheSizeInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltImageCacheCapacityMultiplierWhenPlayingVideo != NULL);
-  EXPECT_TRUE(extension_api->CobaltSkiaGlyphAtlasWidth != NULL);
-  EXPECT_TRUE(extension_api->CobaltSkiaGlyphAtlasHeight != NULL);
-  EXPECT_TRUE(extension_api->CobaltJsGarbageCollectionThresholdInBytes != NULL);
-  EXPECT_TRUE(extension_api->CobaltReduceCpuMemoryBy != NULL);
-  EXPECT_TRUE(extension_api->CobaltReduceGpuMemoryBy != NULL);
-  EXPECT_TRUE(extension_api->CobaltGcZeal != NULL);
+  EXPECT_GE(extension_api->version, 1u);
+  EXPECT_LE(extension_api->version, 2u);
+  EXPECT_NE(extension_api->CobaltUserOnExitStrategy, nullptr);
+  EXPECT_NE(extension_api->CobaltRenderDirtyRegionOnly, nullptr);
+  EXPECT_NE(extension_api->CobaltEglSwapInterval, nullptr);
+  EXPECT_NE(extension_api->CobaltFallbackSplashScreenUrl, nullptr);
+  EXPECT_NE(extension_api->CobaltEnableQuic, nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltOffscreenTargetCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltEncodedImageCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltImageCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltLocalTypefaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltRemoteTypefaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltMeshCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltSoftwareSurfaceCacheSizeInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltImageCacheCapacityMultiplierWhenPlayingVideo,
+            nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasWidth, nullptr);
+  EXPECT_NE(extension_api->CobaltSkiaGlyphAtlasHeight, nullptr);
+  EXPECT_NE(extension_api->CobaltJsGarbageCollectionThresholdInBytes, nullptr);
+  EXPECT_NE(extension_api->CobaltReduceCpuMemoryBy, nullptr);
+  EXPECT_NE(extension_api->CobaltReduceGpuMemoryBy, nullptr);
+  EXPECT_NE(extension_api->CobaltGcZeal, nullptr);
+  if (extension_api->version >= 2) {
+    EXPECT_NE(extension_api->CobaltFallbackSplashScreenTopics, nullptr);
+  }
 
   const ExtensionApi* second_extension_api =
       static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
diff --git a/src/cobalt/extension/graphics.h b/src/cobalt/extension/graphics.h
index 9c993c5..a1a3b4e 100644
--- a/src/cobalt/extension/graphics.h
+++ b/src/cobalt/extension/graphics.h
@@ -23,8 +23,7 @@
 extern "C" {
 #endif
 
-#define kCobaltExtensionGraphicsName \
-  "dev.cobalt.extension.Graphics"
+#define kCobaltExtensionGraphicsName "dev.cobalt.extension.Graphics"
 
 typedef struct CobaltExtensionGraphicsApi {
   // Name should be the string kCobaltExtensionGraphicsName.
@@ -62,6 +61,20 @@
 
   // Get whether the renderer should support 360 degree video or not.
   bool (*IsMapToMeshEnabled)();
+
+  // The fields below this point were added in version 4 or later.
+
+  // Specify whether the framebuffer should be cleared when the graphics
+  // system is shutdown and color to use for clearing. The graphics system
+  // is shutdown on suspend or exit. The clear color values should be in the
+  // range of [0,1]; color values are only used if this function returns true.
+  //
+  // The default behavior is to clear to opaque black on shutdown unless this
+  // API specifies otherwise.
+  bool (*ShouldClearFrameOnShutdown)(float* clear_color_red,
+                                     float* clear_color_green,
+                                     float* clear_color_blue,
+                                     float* clear_color_alpha);
 } CobaltExtensionGraphicsApi;
 
 #ifdef __cplusplus
diff --git a/src/cobalt/layout/box_generator.cc b/src/cobalt/layout/box_generator.cc
index c622e99..2bc8571 100644
--- a/src/cobalt/layout/box_generator.cc
+++ b/src/cobalt/layout/box_generator.cc
@@ -1057,12 +1057,17 @@
   container_box_before_split->SetUiNavItem(html_element->GetUiNavItem());
   boxes_.push_back(container_box_before_split);
 
-  BoxIntersectionObserverModule::IntersectionObserverRootVector roots =
-      html_element->GetLayoutIntersectionObserverRoots();
-  BoxIntersectionObserverModule::IntersectionObserverTargetVector targets =
-      html_element->GetLayoutIntersectionObserverTargets();
-  container_box_before_split->AddIntersectionObserverRootsAndTargets(
-      std::move(roots), std::move(targets));
+  // We already handle the case where the Intersection Observer root is the
+  // viewport with the initial containing block in layout.
+  if (html_element !=
+      html_element->node_document()->document_element()->AsHTMLElement()) {
+    BoxIntersectionObserverModule::IntersectionObserverRootVector roots =
+        html_element->GetLayoutIntersectionObserverRoots();
+    BoxIntersectionObserverModule::IntersectionObserverTargetVector targets =
+        html_element->GetLayoutIntersectionObserverTargets();
+    container_box_before_split->AddIntersectionObserverRootsAndTargets(
+        std::move(roots), std::move(targets));
+  }
 
   AppendPseudoElementToLine(html_element, dom::kBeforePseudoElementType);
 
diff --git a/src/cobalt/layout/intersection_observer_target.cc b/src/cobalt/layout/intersection_observer_target.cc
index 26d863d..e114025 100644
--- a/src/cobalt/layout/intersection_observer_target.cc
+++ b/src/cobalt/layout/intersection_observer_target.cc
@@ -27,108 +27,22 @@
 
 namespace cobalt {
 namespace layout {
+namespace {
 
-void IntersectionObserverTarget::UpdateIntersectionObservationsForTarget(
-    ContainerBox* target_box) {
-  TRACE_EVENT0(
-      "cobalt::layout",
-      "IntersectionObserverTarget::UpdateIntersectionObservationsForTarget()");
-  // Walk up the containing block chain looking for the box referencing the
-  // IntersectionObserverRoot corresponding to this IntersectionObserverTarget.
-  // Skip further processing for the target if it is not a descendant of the
-  // root in the containing block chain.
-  const ContainerBox* root_box = target_box->GetContainingBlock();
-  while (!root_box->ContainsIntersectionObserverRoot(
-      intersection_observer_root_)) {
-    if (!root_box->parent()) {
-      return;
-    }
-    root_box = root_box->GetContainingBlock();
-  }
-
-  // Let targetRect be target's bounding border box.
-  RectLayoutUnit target_transformed_border_box(
-      target_box->GetTransformedBoxFromRoot(
-          target_box->GetBorderBoxFromMarginBox()));
-  math::RectF target_rect =
-      math::RectF(target_transformed_border_box.x().toFloat(),
-                  target_transformed_border_box.y().toFloat(),
-                  target_transformed_border_box.width().toFloat(),
-                  target_transformed_border_box.height().toFloat());
-
-  // Let intersectionRect be the result of running the compute the intersection
-  // algorithm on target.
-  math::RectF root_bounds = GetRootBounds(
-      root_box, intersection_observer_root_->root_margin_property_value());
-  math::RectF intersection_rect = ComputeIntersectionBetweenTargetAndRoot(
-      root_box, root_bounds, target_rect, target_box);
-
-  // Let targetArea be targetRect's area.
-  float target_area = target_rect.size().GetArea();
-
-  // Let intersectionArea be intersectionRect's area.
-  float intersection_area = intersection_rect.size().GetArea();
-
-  // Let isIntersecting be true if targetRect and rootBounds intersect or are
-  // edge-adjacent, even if the intersection has zero area (because rootBounds
-  // or targetRect have zero area); otherwise, let isIntersecting be false.
-  bool is_intersecting =
-      intersection_rect.width() != 0 || intersection_rect.height() != 0;
-
-  // If targetArea is non-zero, let intersectionRatio be intersectionArea
-  // divided by targetArea. Otherwise, let intersectionRatio be 1 if
-  // isIntersecting is true, or 0 if isIntersecting is false.
-  float intersection_ratio = target_area > 0 ? intersection_area / target_area
-                                             : is_intersecting ? 1.0f : 0.0f;
-
-  // Let thresholdIndex be the index of the first entry in observer.thresholds
-  // whose value is greater than intersectionRatio, or the length of
-  // observer.thresholds if intersectionRatio is greater than or equal to the
-  // last entry in observer.thresholds.
-  const std::vector<double>& thresholds =
-      intersection_observer_root_->thresholds_vector();
-  size_t threshold_index;
-  for (threshold_index = 0; threshold_index < thresholds.size();
-       ++threshold_index) {
-    if (thresholds.at(threshold_index) > intersection_ratio) {
-      // isIntersecting is false if intersectionRatio is less than all
-      // thresholds, sorted ascending. Not in spec but follows Chrome behavior.
-      if (threshold_index == 0) {
-        is_intersecting = false;
-      }
-      break;
-    }
-  }
-
-  // If thresholdIndex does not equal previousThresholdIndex or if
-  // isIntersecting does not equal previousIsIntersecting, queue an
-  // IntersectionObserverEntry, passing in observer, time, rootBounds,
-  //         boundingClientRect, intersectionRect, isIntersecting, and target.
-  if (static_cast<int32>(threshold_index) != previous_threshold_index_ ||
-      is_intersecting != previous_is_intersecting_) {
-    on_intersection_callback_.Run(root_bounds, target_rect, intersection_rect,
-                                  is_intersecting, intersection_ratio);
-  }
-
-  // Update the previousThresholdIndex and previousIsIntersecting properties.
-  previous_threshold_index_ = static_cast<int32>(threshold_index);
-  previous_is_intersecting_ = is_intersecting;
+int32 GetUsedLengthOfRootMarginPropertyValue(
+    const scoped_refptr<cssom::PropertyValue>& length_property_value,
+    LayoutUnit percentage_base) {
+  UsedLengthValueProvider used_length_provider(percentage_base);
+  length_property_value->Accept(&used_length_provider);
+  // Not explicitly stated in web spec, but has been observed that Chrome
+  // truncates root margin decimal values.
+  return static_cast<int32>(
+      used_length_provider.used_length().value_or(LayoutUnit(0.0f)).toFloat());
 }
 
-bool IntersectionObserverTarget::IsInContainingBlockChain(
-    const ContainerBox* potential_containing_block,
-    const ContainerBox* target_box) {
-  const ContainerBox* containing_block = target_box->GetContainingBlock();
-  while (containing_block != potential_containing_block) {
-    if (!containing_block->parent()) {
-      return false;
-    }
-    containing_block = containing_block->GetContainingBlock();
-  }
-  return true;
-}
-
-math::RectF IntersectionObserverTarget::GetRootBounds(
+// Rules for determining the root intersection rectangle bounds.
+// https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle
+math::RectF GetRootBounds(
     const ContainerBox* root_box,
     scoped_refptr<cssom::PropertyListValue> root_margin_property_value) {
   math::RectF root_bounds_without_margins;
@@ -175,18 +89,27 @@
   return root_bounds;
 }
 
-int32 IntersectionObserverTarget::GetUsedLengthOfRootMarginPropertyValue(
-    const scoped_refptr<cssom::PropertyValue>& length_property_value,
-    LayoutUnit percentage_base) {
-  UsedLengthValueProvider used_length_provider(percentage_base);
-  length_property_value->Accept(&used_length_provider);
-  // Not explicitly stated in web spec, but has been observed that Chrome
-  // truncates root margin decimal values.
-  return static_cast<int32>(
-      used_length_provider.used_length().value_or(LayoutUnit(0.0f)).toFloat());
+// Similar to the IntersectRects function in math::RectF, but handles edge
+// adjacent intersections as valid intersections (instead of returning a
+// rectangle with zero dimensions)
+math::RectF IntersectIntersectionObserverRects(const math::RectF& a,
+                                               const math::RectF& b) {
+  float rx = std::max(a.x(), b.x());
+  float ry = std::max(a.y(), b.y());
+  float rr = std::min(a.right(), b.right());
+  float rb = std::min(a.bottom(), b.bottom());
+
+  if (rx > rr || ry > rb) {
+    return math::RectF(0.0f, 0.0f, 0.0f, 0.0f);
+  }
+
+  return math::RectF(rx, ry, rr - rx, rb - ry);
 }
 
-math::RectF IntersectionObserverTarget::ComputeIntersectionBetweenTargetAndRoot(
+// Compute the intersection between a target and the observer's intersection
+// root.
+// https://www.w3.org/TR/intersection-observer/#calculate-intersection-rect-algo
+math::RectF ComputeIntersectionBetweenTargetAndRoot(
     const ContainerBox* root_box, const math::RectF& root_bounds,
     const math::RectF& target_rect, const ContainerBox* target_box) {
   // Let intersectionRect be target's bounding border box.
@@ -280,18 +203,95 @@
   return intersection_rect;
 }
 
-math::RectF IntersectionObserverTarget::IntersectIntersectionObserverRects(
-    const math::RectF& a, const math::RectF& b) {
-  float rx = std::max(a.x(), b.x());
-  float ry = std::max(a.y(), b.y());
-  float rr = std::min(a.right(), b.right());
-  float rb = std::min(a.bottom(), b.bottom());
+}  // namespace
 
-  if (rx > rr || ry > rb) {
-    return math::RectF(0.0f, 0.0f, 0.0f, 0.0f);
+void IntersectionObserverTarget::UpdateIntersectionObservationsForTarget(
+    ContainerBox* target_box) {
+  TRACE_EVENT0(
+      "cobalt::layout",
+      "IntersectionObserverTarget::UpdateIntersectionObservationsForTarget()");
+  // Walk up the containing block chain looking for the box referencing the
+  // IntersectionObserverRoot corresponding to this IntersectionObserverTarget.
+  // Skip further processing for the target if it is not a descendant of the
+  // root in the containing block chain.
+  const ContainerBox* root_box = target_box->GetContainingBlock();
+  while (!root_box->ContainsIntersectionObserverRoot(
+      intersection_observer_root_)) {
+    if (!root_box->parent()) {
+      return;
+    }
+    root_box = root_box->GetContainingBlock();
   }
 
-  return math::RectF(rx, ry, rr - rx, rb - ry);
+  // Let targetRect be target's bounding border box.
+  RectLayoutUnit target_transformed_border_box(
+      target_box->GetTransformedBoxFromRoot(
+          target_box->GetBorderBoxFromMarginBox()));
+  const math::RectF target_rect =
+      math::RectF(target_transformed_border_box.x().toFloat(),
+                  target_transformed_border_box.y().toFloat(),
+                  target_transformed_border_box.width().toFloat(),
+                  target_transformed_border_box.height().toFloat());
+
+  // Let intersectionRect be the result of running the compute the intersection
+  // algorithm on target.
+  const math::RectF root_bounds = GetRootBounds(
+      root_box, intersection_observer_root_->root_margin_property_value());
+  const math::RectF intersection_rect = ComputeIntersectionBetweenTargetAndRoot(
+      root_box, root_bounds, target_rect, target_box);
+
+  // Let targetArea be targetRect's area.
+  float target_area = target_rect.size().GetArea();
+
+  // Let intersectionArea be intersectionRect's area.
+  float intersection_area = intersection_rect.size().GetArea();
+
+  // Let isIntersecting be true if targetRect and rootBounds intersect or are
+  // edge-adjacent, even if the intersection has zero area (because rootBounds
+  // or targetRect have zero area); otherwise, let isIntersecting be false.
+  bool is_intersecting =
+      intersection_rect.width() != 0 || intersection_rect.height() != 0 ||
+      (target_rect.width() == 0 && target_rect.height() == 0 &&
+       root_bounds.Contains(target_rect));
+
+  // If targetArea is non-zero, let intersectionRatio be intersectionArea
+  // divided by targetArea. Otherwise, let intersectionRatio be 1 if
+  // isIntersecting is true, or 0 if isIntersecting is false.
+  float intersection_ratio = target_area > 0 ? intersection_area / target_area
+                                             : is_intersecting ? 1.0f : 0.0f;
+
+  // Let thresholdIndex be the index of the first entry in observer.thresholds
+  // whose value is greater than intersectionRatio, or the length of
+  // observer.thresholds if intersectionRatio is greater than or equal to the
+  // last entry in observer.thresholds.
+  const std::vector<double>& thresholds =
+      intersection_observer_root_->thresholds_vector();
+  size_t threshold_index;
+  for (threshold_index = 0; threshold_index < thresholds.size();
+       ++threshold_index) {
+    if (thresholds.at(threshold_index) > intersection_ratio) {
+      // isIntersecting is false if intersectionRatio is less than all
+      // thresholds, sorted ascending. Not in spec but follows Chrome behavior.
+      if (threshold_index == 0) {
+        is_intersecting = false;
+      }
+      break;
+    }
+  }
+
+  // If thresholdIndex does not equal previousThresholdIndex or if
+  // isIntersecting does not equal previousIsIntersecting, queue an
+  // IntersectionObserverEntry, passing in observer, time, rootBounds,
+  //         boundingClientRect, intersectionRect, isIntersecting, and target.
+  if (static_cast<int32>(threshold_index) != previous_threshold_index_ ||
+      is_intersecting != previous_is_intersecting_) {
+    on_intersection_callback_.Run(root_bounds, target_rect, intersection_rect,
+                                  is_intersecting, intersection_ratio);
+  }
+
+  // Update the previousThresholdIndex and previousIsIntersecting properties.
+  previous_threshold_index_ = static_cast<int32>(threshold_index);
+  previous_is_intersecting_ = is_intersecting;
 }
 
 }  // namespace layout
diff --git a/src/cobalt/layout/intersection_observer_target.h b/src/cobalt/layout/intersection_observer_target.h
index a40d41d..23a8671 100644
--- a/src/cobalt/layout/intersection_observer_target.h
+++ b/src/cobalt/layout/intersection_observer_target.h
@@ -75,34 +75,6 @@
   }
 
  private:
-  // Walk up the containing block chain, as described in
-  // http://www.w3.org/TR/CSS2/visudet.html#containing-block-details
-  bool IsInContainingBlockChain(const ContainerBox* potential_containing_block,
-                                const ContainerBox* target_box);
-
-  int32 GetUsedLengthOfRootMarginPropertyValue(
-      const scoped_refptr<cssom::PropertyValue>& length_property_value,
-      LayoutUnit percentage_base);
-
-  // Rules for determining the root intersection rectangle bounds.
-  // https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle
-  math::RectF GetRootBounds(
-      const ContainerBox* root_box,
-      scoped_refptr<cssom::PropertyListValue> root_margin_property_value);
-
-  // Compute the intersection between a target and the observer's intersection
-  // root.
-  // https://www.w3.org/TR/intersection-observer/#calculate-intersection-rect-algo
-  math::RectF ComputeIntersectionBetweenTargetAndRoot(
-      const ContainerBox* root_box, const math::RectF& root_bounds,
-      const math::RectF& target_rect, const ContainerBox* target_box);
-
-  // Similar to the IntersectRects function in math::RectF, but handles edge
-  // adjacent intersections as valid intersections (instead of returning a
-  // rectangle with zero dimensions)
-  math::RectF IntersectIntersectionObserverRects(const math::RectF& a,
-                                                 const math::RectF& b);
-
   OnIntersectionCallback on_intersection_callback_;
 
   scoped_refptr<IntersectionObserverRoot> intersection_observer_root_;
diff --git a/src/cobalt/layout/topmost_event_target.cc b/src/cobalt/layout/topmost_event_target.cc
index df2cd6a..8801fef 100644
--- a/src/cobalt/layout/topmost_event_target.cc
+++ b/src/cobalt/layout/topmost_event_target.cc
@@ -241,9 +241,11 @@
         for (scoped_refptr<dom::Element> element = target_element;
              element != nearest_common_ancestor;
              element = element->parent_element()) {
-          element->DispatchEvent(new dom::PointerEvent(
-              base::Tokens::pointerenter(), dom::Event::kNotBubbles,
-              dom::Event::kNotCancelable, view, *event_init));
+          if (element) {
+            element->DispatchEvent(new dom::PointerEvent(
+                base::Tokens::pointerenter(), dom::Event::kNotBubbles,
+                dom::Event::kNotCancelable, view, *event_init));
+          }
         }
       }
 
@@ -254,9 +256,11 @@
       for (scoped_refptr<dom::Element> element = target_element;
            element != nearest_common_ancestor;
            element = element->parent_element()) {
-        element->DispatchEvent(new dom::MouseEvent(
-            base::Tokens::mouseenter(), dom::Event::kNotBubbles,
-            dom::Event::kNotCancelable, view, *event_init));
+        if (element) {
+          element->DispatchEvent(new dom::MouseEvent(
+              base::Tokens::mouseenter(), dom::Event::kNotBubbles,
+              dom::Event::kNotCancelable, view, *event_init));
+        }
       }
     }
   }
diff --git a/src/cobalt/layout_tests/testdata/web-platform-tests/cobalt_special/web_platform_tests.txt b/src/cobalt/layout_tests/testdata/web-platform-tests/cobalt_special/web_platform_tests.txt
index 25ffd6c..d6e13da 100644
--- a/src/cobalt/layout_tests/testdata/web-platform-tests/cobalt_special/web_platform_tests.txt
+++ b/src/cobalt/layout_tests/testdata/web-platform-tests/cobalt_special/web_platform_tests.txt
@@ -1,4 +1,5 @@
 # Cobalt's special tests that borrows WPT infrastructures.
 
 origin-clean.htm,PASS
-preflight-cache-2.htm,PASS
\ No newline at end of file
+preflight-cache-2.htm,PASS
+xhr_content_length.htm,PASS
diff --git a/src/cobalt/layout_tests/testdata/web-platform-tests/intersection-observer/web_platform_tests.txt b/src/cobalt/layout_tests/testdata/web-platform-tests/intersection-observer/web_platform_tests.txt
index b830268..e543994 100644
--- a/src/cobalt/layout_tests/testdata/web-platform-tests/intersection-observer/web_platform_tests.txt
+++ b/src/cobalt/layout_tests/testdata/web-platform-tests/intersection-observer/web_platform_tests.txt
@@ -7,33 +7,33 @@
 # Empty rootMargin should evaluate to default, not cause error
 empty-root-margin.html,DISABLE
 initial-observation-with-threshold.html,PASS
+# Unsupported functions measuring space width between adjacent inline elements
+inline-client-rect.html,DISABLE
 inline-with-block-child-client-rect.html,PASS
 isIntersecting-change-events.html,PASS
+# overflow: scroll results in incorrectly clipped intersection rect
+isIntersecting-threshold.html,DISABLE
+multiple-targets.html,PASS
+multiple-thresholds.html,PASS
 # rootMargin default should be "0px 0px 0px 0px", not "0px"
 observer-attributes.html,DISABLE
 # WPT testharness needs to be rebased
 observer-exceptions.html,DISABLE
+observer-without-js-reference.html,PASS
 #Deleting an element does not trigger an intersection
 remove-element.html,DISABLE
 root-margin-root-element.html,PASS
+# Root margin calculations have rounding errors
+root-margin.html,DISABLE
 # Setting IO target equal to document.documentElement crashes Cobalt
 root-margin-rounding.html,DISABLE
 rtl-clipped-root.html,PASS
+same-document-no-root.html,PASS
 same-document-root.html,PASS
+same-document-zero-size-target.html,PASS
+text-target.html,PASS
 zero-area-element-hidden.html,PASS
-#Zero-area target does not trigger an intersection
-zero-area-element-visible.html,DISABLE
-
-#No root specified - intersections with viewport incorrectly reported
-inline-client-rect.html,DISABLE
-isIntersecting-threshold.html,DISABLE
-multiple-targets.html,DISABLE
-multiple-thresholds.html,DISABLE
-observer-without-js-reference.html,DISABLE
-root-margin.html,DISABLE
-same-document-no-root.html,DISABLE
-same-document-zero-size-target.html,DISABLE
-text-target.html,DISABLE
+zero-area-element-visible.html,PASS
 
 #IntersectionObserverV2 not implemented
 v2/blur-filter.html,DISABLE
diff --git a/src/cobalt/media/base/sbplayer_pipeline.cc b/src/cobalt/media/base/sbplayer_pipeline.cc
index 4f4f9d1..8185f82 100644
--- a/src/cobalt/media/base/sbplayer_pipeline.cc
+++ b/src/cobalt/media/base/sbplayer_pipeline.cc
@@ -277,8 +277,13 @@
 
   VideoFrameProvider* video_frame_provider_;
 
+  // Read audio from the stream if |timestamp_of_last_written_audio_| is less
+  // than |seek_time_| + |kAudioPrerollLimit|, this effectively allows 10
+  // seconds of audio to be written to the SbPlayer after playback startup or
+  // seek.
+  static const SbTime kAudioPrerollLimit = 10 * kSbTimeSecond;
   // Don't read audio from the stream more than |kAudioLimit| ahead of the
-  // current media time.
+  // current media time during playing.
   static const SbTime kAudioLimit = kSbTimeSecond;
   // Only call GetMediaTime() from OnNeedData if it has been
   // |kMediaTimeCheckInterval| since the last call to GetMediaTime().
@@ -1117,22 +1122,28 @@
         kMediaTimeCheckInterval) {
       GetMediaTime();
     }
-    // The estimated time ahead of playback may be negative if no audio has been
-    // written.
-    SbTime time_ahead_of_playback =
-        timestamp_of_last_written_audio_ - last_media_time_;
-    // Delay reading audio more than |kAudioLimit| ahead of playback, taking
-    // into account that our estimate of playback time might be behind by
+
+    // Delay reading audio more than |kAudioLimit| ahead of playback after the
+    // player has received enough audio for preroll, taking into account that
+    // our estimate of playback time might be behind by
     // |kMediaTimeCheckInterval|.
-    if (time_ahead_of_playback > (kAudioLimit + kMediaTimeCheckInterval)) {
-      SbTime delay_time = (time_ahead_of_playback - kAudioLimit) /
-                          std::max(playback_rate_, 1.0f);
-      task_runner_->PostDelayedTask(
-          FROM_HERE, base::Bind(&SbPlayerPipeline::DelayedNeedData, this),
-          base::TimeDelta::FromMicroseconds(delay_time));
-      audio_read_delayed_ = true;
-      return;
+    if (timestamp_of_last_written_audio_ - seek_time_.ToSbTime() >
+        kAudioPrerollLimit) {
+      // The estimated time ahead of playback may be negative if no audio has
+      // been written.
+      SbTime time_ahead_of_playback =
+          timestamp_of_last_written_audio_ - last_media_time_;
+      if (time_ahead_of_playback > (kAudioLimit + kMediaTimeCheckInterval)) {
+        SbTime delay_time = (time_ahead_of_playback - kAudioLimit) /
+                            std::max(playback_rate_, 1.0f);
+        task_runner_->PostDelayedTask(
+            FROM_HERE, base::Bind(&SbPlayerPipeline::DelayedNeedData, this),
+            base::TimeDelta::FromMicroseconds(delay_time));
+        audio_read_delayed_ = true;
+        return;
+      }
     }
+
     audio_read_delayed_ = false;
 #endif  // SB_API_VERSION >= 11
     audio_read_in_progress_ = true;
diff --git a/src/cobalt/renderer/glimp_shaders/glsl/shaders.gypi b/src/cobalt/renderer/glimp_shaders/glsl/shaders.gypi
index 5e37306..a8801f8 100644
--- a/src/cobalt/renderer/glimp_shaders/glsl/shaders.gypi
+++ b/src/cobalt/renderer/glimp_shaders/glsl/shaders.gypi
@@ -21,8 +21,11 @@
 {
   'variables': {
     'glsl_shaders_dir': '<(DEPTH)/cobalt/renderer/glimp_shaders/glsl',
+    'glsl_shaders_0': [
+        '<!@pymod_do_main(starboard.build.gyp_functions file_glob <(DEPTH)/cobalt/renderer/glimp_shaders/glsl/ *.glsl)'
+    ],
     'glsl_shaders': [
-        '<!@(ls -1 <(DEPTH)/cobalt/renderer/glimp_shaders/glsl/*.glsl |xargs -n 1 basename)',
+        '<!@pymod_do_main(starboard.build.gyp_functions basename <@(glsl_shaders_0) )',
     ],
   }
 }
diff --git a/src/cobalt/renderer/pipeline.cc b/src/cobalt/renderer/pipeline.cc
index 97bef85..a78f0ed 100644
--- a/src/cobalt/renderer/pipeline.cc
+++ b/src/cobalt/renderer/pipeline.cc
@@ -24,12 +24,14 @@
 #include "cobalt/base/address_sanitizer.h"
 #include "cobalt/base/cobalt_paths.h"
 #include "cobalt/base/polymorphic_downcast.h"
+#include "cobalt/extension/graphics.h"
 #include "cobalt/math/rect_f.h"
 #include "cobalt/render_tree/brush.h"
 #include "cobalt/render_tree/composition_node.h"
 #include "cobalt/render_tree/dump_render_tree_to_string.h"
 #include "cobalt/render_tree/rect_node.h"
 #include "nb/memory_scope.h"
+#include "starboard/system.h"
 
 using cobalt::render_tree::Node;
 using cobalt::render_tree::animations::AnimateNode;
@@ -38,6 +40,7 @@
 namespace renderer {
 
 namespace {
+
 #if !defined(COBALT_MINIMUM_FRAME_TIME_IN_MILLISECONDS)
 // This default value has been moved from cobalt/build/cobalt_configuration.gypi
 // in favor of the usage of
@@ -76,6 +79,34 @@
   }
 }
 
+bool ShouldClearFrameOnShutdown(render_tree::ColorRGBA* out_clear_color) {
+#if SB_API_VERSION >= 11
+  const CobaltExtensionGraphicsApi* graphics_extension =
+      static_cast<const CobaltExtensionGraphicsApi*>(
+          SbSystemGetExtension(kCobaltExtensionGraphicsName));
+  if (graphics_extension &&
+      strcmp(graphics_extension->name, kCobaltExtensionGraphicsName) == 0 &&
+      graphics_extension->version >= 4) {
+    float r, g, b, a;
+    if (graphics_extension->ShouldClearFrameOnShutdown(&r, &g, &b, &a)) {
+      out_clear_color->set_r(r);
+      out_clear_color->set_g(g);
+      out_clear_color->set_b(b);
+      out_clear_color->set_a(a);
+      return true;
+    }
+    return false;
+  }
+#endif
+
+  // Default is to clear to opaque black.
+  out_clear_color->set_r(0.0f);
+  out_clear_color->set_g(0.0f);
+  out_clear_color->set_b(0.0f);
+  out_clear_color->set_a(1.0f);
+  return true;
+}
+
 }  // namespace
 
 Pipeline::Pipeline(const CreateRasterizerFunction& create_rasterizer_function,
@@ -329,13 +360,13 @@
       minimum_frame_interval_milliseconds =
           COBALT_MINIMUM_FRAME_TIME_IN_MILLISECONDS;
     } else {
-      DLOG(ERROR) <<
-          "COBALT_MINIMUM_FRAME_TIME_IN_MILLISECONDS and "
-          "CobaltExtensionGraphicsApi::GetMinimumFrameIntervalInMilliseconds"
-          "are both defined."
-          "Remove the 'cobalt_minimum_frame_time_in_milliseconds' ";
-          "from ../gyp_configuration.gypi in favor of the usage of "
-          "CobaltExtensionGraphicsApi::GetMinimumFrameIntervalInMilliseconds."
+      DLOG(ERROR)
+          << "COBALT_MINIMUM_FRAME_TIME_IN_MILLISECONDS and "
+             "CobaltExtensionGraphicsApi::GetMinimumFrameIntervalInMilliseconds"
+             "are both defined."
+             "Remove the 'cobalt_minimum_frame_time_in_milliseconds' ";
+      "from ../gyp_configuration.gypi in favor of the usage of "
+      "CobaltExtensionGraphicsApi::GetMinimumFrameIntervalInMilliseconds."
     }
 #else
     if (minimum_frame_interval_milliseconds < 0.0f) {
@@ -373,11 +404,13 @@
   bool is_new_render_tree = submission.render_tree != last_render_tree_;
   bool has_render_tree_changed =
       !last_animations_expired_ || is_new_render_tree;
-  bool force_rasterize = submit_even_if_render_tree_is_unchanged_ ||
-      fps_overlay_update_pending_;
+  bool force_rasterize =
+      submit_even_if_render_tree_is_unchanged_ || fps_overlay_update_pending_;
 
-  float maximum_frame_interval_milliseconds = graphics_context_ ?
-      graphics_context_->GetMaximumFrameIntervalInMilliseconds() : -1.0f;
+  float maximum_frame_interval_milliseconds =
+      graphics_context_
+          ? graphics_context_->GetMaximumFrameIntervalInMilliseconds()
+          : -1.0f;
   if (maximum_frame_interval_milliseconds >= 0.0f) {
     base::TimeDelta max_time_between_rasterize =
         base::TimeDelta::FromMillisecondsD(maximum_frame_interval_milliseconds);
@@ -617,18 +650,18 @@
   // Shutdown the FPS overlay which may reference render trees.
   fps_overlay_ = base::nullopt;
 
-  // Submit a black fullscreen rect node to clear the display before shutting
+  // Submit a fullscreen rect node to clear the display before shutting
   // down.  This can be helpful if we quit while playing a video via
   // punch-through, which may result in unexpected images/colors appearing for
   // a flicker behind the display.
-  if (render_target_ && (clear_on_shutdown_mode_ == kClearToBlack)) {
-    rasterizer_->Submit(
-        new render_tree::RectNode(
-            math::RectF(render_target_->GetSize()),
-            std::unique_ptr<render_tree::Brush>(
-                new render_tree::SolidColorBrush(
-                    render_tree::ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f)))),
-        render_target_);
+  render_tree::ColorRGBA clear_color;
+  if (render_target_ && clear_on_shutdown_mode_ == kClearAccordingToPlatform &&
+      ShouldClearFrameOnShutdown(&clear_color)) {
+    rasterizer_->Submit(new render_tree::RectNode(
+                            math::RectF(render_target_->GetSize()),
+                            std::unique_ptr<render_tree::Brush>(
+                                new render_tree::SolidColorBrush(clear_color))),
+                        render_target_);
   }
 
   // This potential reference to a render tree whose animations may have ended
diff --git a/src/cobalt/renderer/pipeline.h b/src/cobalt/renderer/pipeline.h
index bbb9658..5848d95 100644
--- a/src/cobalt/renderer/pipeline.h
+++ b/src/cobalt/renderer/pipeline.h
@@ -58,7 +58,12 @@
       RasterizationCompleteCallback;
 
   enum ShutdownClearMode {
-    kClearToBlack,
+    // Query CobaltExtensionGraphicsApi's ShouldClearFrameOnShutdown for
+    // shutdown behavior.
+    kClearAccordingToPlatform,
+
+    // Do not clear regardless of what CobaltExtensionGraphicsApi's
+    // ShouldClearFrameOnShutdown specifies.
     kNoClear,
   };
 
diff --git a/src/cobalt/renderer/rasterizer/egl/hardware_rasterizer.cc b/src/cobalt/renderer/rasterizer/egl/hardware_rasterizer.cc
index 801112c..1f04c7a 100644
--- a/src/cobalt/renderer/rasterizer/egl/hardware_rasterizer.cc
+++ b/src/cobalt/renderer/rasterizer/egl/hardware_rasterizer.cc
@@ -234,7 +234,7 @@
   uint32_t untouched_states =
       kMSAAEnable_GrGLBackendState | kStencil_GrGLBackendState |
       kPixelStore_GrGLBackendState | kFixedFunction_GrGLBackendState |
-      kPathRendering_GrGLBackendState | kMisc_GrGLBackendState;
+      kPathRendering_GrGLBackendState;
 
   GetFallbackContext()->resetContext(~untouched_states & kAll_GrBackendState);
 }
diff --git a/src/cobalt/renderer/renderer_module.cc b/src/cobalt/renderer/renderer_module.cc
index 6b52456..9783414 100644
--- a/src/cobalt/renderer/renderer_module.cc
+++ b/src/cobalt/renderer/renderer_module.cc
@@ -102,7 +102,7 @@
         // deprecate the submit_even_if_render_tree_is_unchanged.
         false,
 #endif
-        renderer::Pipeline::kClearToBlack, pipeline_options));
+        renderer::Pipeline::kClearAccordingToPlatform, pipeline_options));
   }
 }
 
diff --git a/src/cobalt/site/docs/development/setup-linux.md b/src/cobalt/site/docs/development/setup-linux.md
index 3c4d49f..4fff20e 100644
--- a/src/cobalt/site/docs/development/setup-linux.md
+++ b/src/cobalt/site/docs/development/setup-linux.md
@@ -32,12 +32,14 @@
     Cobalt on Linux:
 
     ```
-    $ sudo apt install -qqy --no-install-recommends pkgconf ninja-build bison \
-        yasm binutils clang libgles2-mesa-dev mesa-common-dev libpulse-dev \
-        libavresample-dev libasound2-dev libxrender-dev libxcomposite-dev
+    $ sudo apt install -qqy --no-install-recommends pkgconf ninja-build \
+        bison yasm binutils clang libgles2-mesa-dev mesa-common-dev \
+        libpulse-dev libavresample-dev libasound2-dev libxrender-dev \
+        libxcomposite-dev
     ```
 
 1.  Install Node.js via `nvm`:
+
     ```
     $ export NVM_DIR=~/.nvm
     $ export NODE_VERSION=12.17.0
diff --git a/src/cobalt/site/docs/reference/starboard/modules/10/media.md b/src/cobalt/site/docs/reference/starboard/modules/10/media.md
index 7271b29..b9019a6 100644
--- a/src/cobalt/site/docs/reference/starboard/modules/10/media.md
+++ b/src/cobalt/site/docs/reference/starboard/modules/10/media.md
@@ -435,11 +435,39 @@
 `mime`: The mime information of the media in the form of `video/webm` or
 `video/mp4; codecs="avc1.42001E"`. It may include arbitrary parameters like
 "codecs", "channels", etc. Note that the "codecs" parameter may contain more
-than one codec, delimited by comma. `key_system`: A lowercase value in fhe form
+than one codec, delimited by comma. `key_system`: A lowercase value in the form
 of "com.example.somesystem" as suggested by [https://w3c.github.io/encrypted-media/#key-system](https://w3c.github.io/encrypted-media/#key-system)) that can
 be matched exactly with known DRM key systems of the platform. When `key_system`
 is an empty string, the return value is an indication for non-encrypted media.
 
+An implementation may choose to support `key_system` with extra attributes,
+separated by ';', like `com.example.somesystem; attribute_name1="value1";
+attribute_name2=value1`. If `key_system` with attributes is not supported by an
+implementation, it should treat `key_system` as if it contains only the key
+system, and reject any input containing extra attributes, i.e. it can keep using
+its existing implementation. When an implementation supports `key_system` with
+attributes, it has to support all attributes defined by the Starboard version
+the implementation uses. An implementation should ignore any unknown attributes,
+and make a decision solely based on the key system and the known attributes. For
+example, if an implementation supports "com.widevine.alpha", it should also
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; invalid_attribute="invalid_value"`.
+Currently the only attribute has to be supported is `encryptionscheme`. It
+reflects the value passed to `encryptionScheme` encryptionScheme of
+MediaKeySystemMediaCapability, as defined in [https://wicg.github.io/encrypted-media-encryption-scheme/,](https://wicg.github.io/encrypted-media-encryption-scheme/,),) which can take value "cenc", "cbcs", or "cbcs-1-9". Empty string is
+not a valid value for `encryptionscheme` and the implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`encryptionscheme` is set to "". The implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported for unknown
+values of known attributes. For example, if an implementation supports
+"encryptionscheme" with value "cenc", "cbcs", or "cbcs-1-9", then it should
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; encryptionscheme="cenc"`, and return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`key_system` is `com.widevine.alpha; encryptionscheme="invalid"`. If an
+implementation supports key system with attributes on one key system, it has to
+support key system with attributes on all key systems supported.
+
 #### Declaration ####
 
 ```
diff --git a/src/cobalt/site/docs/reference/starboard/modules/11/media.md b/src/cobalt/site/docs/reference/starboard/modules/11/media.md
index 377cb2b..22da43e 100644
--- a/src/cobalt/site/docs/reference/starboard/modules/11/media.md
+++ b/src/cobalt/site/docs/reference/starboard/modules/11/media.md
@@ -429,11 +429,39 @@
 `mime`: The mime information of the media in the form of `video/webm` or
 `video/mp4; codecs="avc1.42001E"`. It may include arbitrary parameters like
 "codecs", "channels", etc. Note that the "codecs" parameter may contain more
-than one codec, delimited by comma. `key_system`: A lowercase value in fhe form
+than one codec, delimited by comma. `key_system`: A lowercase value in the form
 of "com.example.somesystem" as suggested by [https://w3c.github.io/encrypted-media/#key-system](https://w3c.github.io/encrypted-media/#key-system)) that can
 be matched exactly with known DRM key systems of the platform. When `key_system`
 is an empty string, the return value is an indication for non-encrypted media.
 
+An implementation may choose to support `key_system` with extra attributes,
+separated by ';', like `com.example.somesystem; attribute_name1="value1";
+attribute_name2=value1`. If `key_system` with attributes is not supported by an
+implementation, it should treat `key_system` as if it contains only the key
+system, and reject any input containing extra attributes, i.e. it can keep using
+its existing implementation. When an implementation supports `key_system` with
+attributes, it has to support all attributes defined by the Starboard version
+the implementation uses. An implementation should ignore any unknown attributes,
+and make a decision solely based on the key system and the known attributes. For
+example, if an implementation supports "com.widevine.alpha", it should also
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; invalid_attribute="invalid_value"`.
+Currently the only attribute has to be supported is `encryptionscheme`. It
+reflects the value passed to `encryptionScheme` encryptionScheme of
+MediaKeySystemMediaCapability, as defined in [https://wicg.github.io/encrypted-media-encryption-scheme/,](https://wicg.github.io/encrypted-media-encryption-scheme/,),) which can take value "cenc", "cbcs", or "cbcs-1-9". Empty string is
+not a valid value for `encryptionscheme` and the implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`encryptionscheme` is set to "". The implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported for unknown
+values of known attributes. For example, if an implementation supports
+"encryptionscheme" with value "cenc", "cbcs", or "cbcs-1-9", then it should
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; encryptionscheme="cenc"`, and return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`key_system` is `com.widevine.alpha; encryptionscheme="invalid"`. If an
+implementation supports key system with attributes on one key system, it has to
+support key system with attributes on all key systems supported.
+
 #### Declaration ####
 
 ```
diff --git a/src/cobalt/site/docs/reference/starboard/modules/12/media.md b/src/cobalt/site/docs/reference/starboard/modules/12/media.md
index 5a50b33..1ca8e06 100644
--- a/src/cobalt/site/docs/reference/starboard/modules/12/media.md
+++ b/src/cobalt/site/docs/reference/starboard/modules/12/media.md
@@ -429,11 +429,39 @@
 `mime`: The mime information of the media in the form of `video/webm` or
 `video/mp4; codecs="avc1.42001E"`. It may include arbitrary parameters like
 "codecs", "channels", etc. Note that the "codecs" parameter may contain more
-than one codec, delimited by comma. `key_system`: A lowercase value in fhe form
+than one codec, delimited by comma. `key_system`: A lowercase value in the form
 of "com.example.somesystem" as suggested by [https://w3c.github.io/encrypted-media/#key-system](https://w3c.github.io/encrypted-media/#key-system)) that can
 be matched exactly with known DRM key systems of the platform. When `key_system`
 is an empty string, the return value is an indication for non-encrypted media.
 
+An implementation may choose to support `key_system` with extra attributes,
+separated by ';', like `com.example.somesystem; attribute_name1="value1";
+attribute_name2=value1`. If `key_system` with attributes is not supported by an
+implementation, it should treat `key_system` as if it contains only the key
+system, and reject any input containing extra attributes, i.e. it can keep using
+its existing implementation. When an implementation supports `key_system` with
+attributes, it has to support all attributes defined by the Starboard version
+the implementation uses. An implementation should ignore any unknown attributes,
+and make a decision solely based on the key system and the known attributes. For
+example, if an implementation supports "com.widevine.alpha", it should also
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; invalid_attribute="invalid_value"`.
+Currently the only attribute has to be supported is `encryptionscheme`. It
+reflects the value passed to `encryptionScheme` encryptionScheme of
+MediaKeySystemMediaCapability, as defined in [https://wicg.github.io/encrypted-media-encryption-scheme/,](https://wicg.github.io/encrypted-media-encryption-scheme/,),) which can take value "cenc", "cbcs", or "cbcs-1-9". Empty string is
+not a valid value for `encryptionscheme` and the implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`encryptionscheme` is set to "". The implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported for unknown
+values of known attributes. For example, if an implementation supports
+"encryptionscheme" with value "cenc", "cbcs", or "cbcs-1-9", then it should
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; encryptionscheme="cenc"`, and return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`key_system` is `com.widevine.alpha; encryptionscheme="invalid"`. If an
+implementation supports key system with attributes on one key system, it has to
+support key system with attributes on all key systems supported.
+
 #### Declaration ####
 
 ```
diff --git a/src/cobalt/site/docs/reference/starboard/modules/media.md b/src/cobalt/site/docs/reference/starboard/modules/media.md
index 5a50b33..1ca8e06 100644
--- a/src/cobalt/site/docs/reference/starboard/modules/media.md
+++ b/src/cobalt/site/docs/reference/starboard/modules/media.md
@@ -429,11 +429,39 @@
 `mime`: The mime information of the media in the form of `video/webm` or
 `video/mp4; codecs="avc1.42001E"`. It may include arbitrary parameters like
 "codecs", "channels", etc. Note that the "codecs" parameter may contain more
-than one codec, delimited by comma. `key_system`: A lowercase value in fhe form
+than one codec, delimited by comma. `key_system`: A lowercase value in the form
 of "com.example.somesystem" as suggested by [https://w3c.github.io/encrypted-media/#key-system](https://w3c.github.io/encrypted-media/#key-system)) that can
 be matched exactly with known DRM key systems of the platform. When `key_system`
 is an empty string, the return value is an indication for non-encrypted media.
 
+An implementation may choose to support `key_system` with extra attributes,
+separated by ';', like `com.example.somesystem; attribute_name1="value1";
+attribute_name2=value1`. If `key_system` with attributes is not supported by an
+implementation, it should treat `key_system` as if it contains only the key
+system, and reject any input containing extra attributes, i.e. it can keep using
+its existing implementation. When an implementation supports `key_system` with
+attributes, it has to support all attributes defined by the Starboard version
+the implementation uses. An implementation should ignore any unknown attributes,
+and make a decision solely based on the key system and the known attributes. For
+example, if an implementation supports "com.widevine.alpha", it should also
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; invalid_attribute="invalid_value"`.
+Currently the only attribute has to be supported is `encryptionscheme`. It
+reflects the value passed to `encryptionScheme` encryptionScheme of
+MediaKeySystemMediaCapability, as defined in [https://wicg.github.io/encrypted-media-encryption-scheme/,](https://wicg.github.io/encrypted-media-encryption-scheme/,),) which can take value "cenc", "cbcs", or "cbcs-1-9". Empty string is
+not a valid value for `encryptionscheme` and the implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`encryptionscheme` is set to "". The implementation should return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported for unknown
+values of known attributes. For example, if an implementation supports
+"encryptionscheme" with value "cenc", "cbcs", or "cbcs-1-9", then it should
+return `kSbMediaSupportTypeProbably` kSbMediaSupportTypeProbably when
+`key_system` is `com.widevine.alpha; encryptionscheme="cenc"`, and return
+`kSbMediaSupportTypeNotSupported` kSbMediaSupportTypeNotSupported when
+`key_system` is `com.widevine.alpha; encryptionscheme="invalid"`. If an
+implementation supports key system with attributes on one key system, it has to
+support key system with attributes on all key systems supported.
+
 #### Declaration ####
 
 ```
diff --git a/src/cobalt/tools/automated_testing/cobalt_runner.py b/src/cobalt/tools/automated_testing/cobalt_runner.py
index 4f627f7..22479f7 100644
--- a/src/cobalt/tools/automated_testing/cobalt_runner.py
+++ b/src/cobalt/tools/automated_testing/cobalt_runner.py
@@ -9,10 +9,10 @@
 import os
 import re
 import sys
-import thread
 import threading
 import time
 import traceback
+import thread
 
 import _env  # pylint: disable=unused-import
 from cobalt.tools.automated_testing import c_val_names
@@ -26,8 +26,8 @@
 RE_WEBDRIVER_FAILED = re.compile(r'Could not start WebDriver server')
 # Pattern to match Cobalt log line for when a WindowDriver has been created.
 RE_WINDOWDRIVER_CREATED = re.compile(
-    r'^\[[\d:]+/[\d.]+:INFO:browser_module\.cc\(\d+\)\] Created WindowDriver: ID=\S+'
-)
+    (r'^\[[\d:]+/[\d.]+:INFO:browser_module\.cc\(\d+\)\] Created WindowDriver: '
+     r'ID=\S+'))
 # Pattern to match Cobalt log line for when a WebModule is has been loaded.
 RE_WEBMODULE_LOADED = re.compile(
     r'^\[[\d:]+/[\d.]+:INFO:browser_module\.cc\(\d+\)\] Loaded WebModule')
@@ -231,7 +231,7 @@
       self.WaitForStart()
     except KeyboardInterrupt:
       # potentially from thread.interrupt_main(). We will treat as
-      # a timeout regardless
+      # a timeout regardless.
 
       self.Exit(should_fail=True)
       raise TimeoutException
@@ -270,8 +270,8 @@
       self.runner_thread.join(COBALT_EXIT_TIMEOUT_SECONDS)
     if self.runner_thread.isAlive():
       sys.stderr.write(
-          '***Runner thread still alive after sending graceful shutdown command, try again by killing app***\n'
-      )
+          '***Runner thread still alive after sending graceful shutdown '
+          'command, try again by killing app***\n')
       self.launcher.Kill()
     # Once the write end of the pipe has been closed by the launcher, the reader
     # thread will get EOF and exit.
@@ -312,7 +312,7 @@
       logging.info('Cobalt terminated.')
       if not self.failed and self.success_message:
         print('{}\n'.format(self.success_message))
-        logging.info('{}\n'.format(self.success_message))
+        logging.info('%s\n', self.success_message)
     # pylint: disable=broad-except
     except Exception as ex:
       sys.stderr.write('Exception running Cobalt ' + str(ex))
@@ -329,7 +329,11 @@
 
   def ExecuteJavaScript(self, js_code):
     retry_count = 0
-    while retry_count < EXECUTE_JAVASCRIPT_RETRY_LIMIT:
+    while True:
+      if retry_count >= EXECUTE_JAVASCRIPT_RETRY_LIMIT:
+        raise TimeoutException(
+            'Selenium element or window not found in {} tries'.format(
+                EXECUTE_JAVASCRIPT_RETRY_LIMIT))
       retry_count += 1
       try:
         result = self.webdriver.execute_script(js_code)
@@ -337,9 +341,6 @@
               selenium_exceptions.NoSuchWindowException):
         time.sleep(0.2)
         continue
-      except Exception:
-        sys.excepthook(*sys.exc_info())
-        logging.exception("Failed with unexpected exception")
       break
     return result
 
@@ -354,15 +355,14 @@
     """
     javascript_code = 'return h5vcc.cVal.getValue(\'{}\')'.format(cval_name)
     cval_string = self.ExecuteJavaScript(javascript_code)
-    if cval_string is None:
-      return None
-    else:
+    if cval_string:
       try:
         # Try to parse numbers and booleans.
         return json.loads(cval_string)
       except ValueError:
         # If we can't parse a value, return the cval string as-is.
         return cval_string
+    return None
 
   def GetCvalBatch(self, cval_name_list):
     """Retrieves a batch of cvals.
@@ -454,7 +454,11 @@
     # after navigation. We only introduced it because of limited time budget
     # at the moment, please don't introduce any code that relies on it.
     retry_count = 0
-    while retry_count < FIND_ELEMENT_RETRY_LIMIT:
+    while True:
+      if retry_count >= FIND_ELEMENT_RETRY_LIMIT:
+        raise TimeoutException(
+            'Selenium element or window not found in {} tries'.format(
+                FIND_ELEMENT_RETRY_LIMIT))
       retry_count += 1
       try:
         elements = self.webdriver.find_elements_by_css_selector(css_selector)
@@ -462,11 +466,8 @@
               selenium_exceptions.NoSuchWindowException):
         time.sleep(0.2)
         continue
-      except Exception:
-        sys.excepthook(*sys.exc_info())
-        logging.exception("Failed with unexpected exception")
       break
-    if expected_num is not None and len(elements) != expected_num:
+    if expected_num and len(elements) != expected_num:
       raise CobaltRunner.AssertException(
           'Expected number of element {} is: {}, got {}'.format(
               css_selector, expected_num, len(elements)))
diff --git a/src/cobalt/updater/configurator.cc b/src/cobalt/updater/configurator.cc
index 210f088..a10bd03 100644
--- a/src/cobalt/updater/configurator.cc
+++ b/src/cobalt/updater/configurator.cc
@@ -1,4 +1,4 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
+// Copyright 2020 The Cobalt Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -28,33 +28,35 @@
 // Default time constants.
 const int kDelayOneMinute = 60;
 const int kDelayOneHour = kDelayOneMinute * 60;
+const std::set<std::string> valid_channels = {
+    // Default channel for debug/devel builds.
+    "dev",
+    // Channel for dogfooders.
+    "dogfood",
+    // Default channel for gold builds.
+    "prod",
+    // Default channel for qa builds. A gold build can switch to this channel to
+    // get an official qa build.
+    "qa",
+    // Test an update with higher version than prod channel.
+    "test",
+    // Test an update with mismatched sabi.
+    "tmsabi",
+    // Test an update that does nothing.
+    "tnoop",
+    // Test an update that crashes.
+    "tcrash",
+    // Test an update that fails verification.
+    "tfailv",
+    // Test a series of continuous updates with two channels.
+    "tseries1", "tseries2",
+};
 
 #if defined(COBALT_BUILD_TYPE_DEBUG) || defined(COBALT_BUILD_TYPE_DEVEL)
-const std::set<std::string> valid_channels = {"dev"};
 const std::string kDefaultUpdaterChannel = "dev";
 #elif defined(COBALT_BUILD_TYPE_QA)
-// Find more information about these test channels in the Evergreen test plan.
-const std::set<std::string> valid_channels = {
-    "qa",
-    // A normal test channel that serves a valid update
-    "test",
-    // Test an update with mismatched sabi
-    "tmsabi",
-    // Test an update that does nothing
-    "tnoop",
-    // Test an update that crashes
-    "tcrash",
-    // Test an update that fails verification
-    "tfailv",
-    // Test a series of continuous updates with two channels
-    "tseries1",
-    "tseries2",
-    // Test an update that's larger than the available storage on the device
-    "tistore",
-};
 const std::string kDefaultUpdaterChannel = "qa";
 #elif defined(COBALT_BUILD_TYPE_GOLD)
-const std::set<std::string> valid_channels = {"prod", "dogfood"};
 const std::string kDefaultUpdaterChannel = "prod";
 #endif
 
@@ -146,8 +148,8 @@
   base::flat_map<std::string, std::string> params;
   params.insert(std::make_pair("SABI", SB_SABI_JSON_ID));
   params.insert(std::make_pair("sbversion", std::to_string(SB_API_VERSION)));
-  params.insert(std::make_pair(
-      "jsengine", script::GetJavaScriptEngineNameAndVersion()));
+  params.insert(
+      std::make_pair("jsengine", script::GetJavaScriptEngineNameAndVersion()));
   params.insert(std::make_pair(
       "updaterchannelchanged",
       SbAtomicNoBarrier_Load(&is_channel_changed_) == 1 ? "True" : "False"));
@@ -252,5 +254,16 @@
   updater_status_ = status;
 }
 
+std::string Configurator::GetPreviousUpdaterStatus() const {
+  base::AutoLock auto_lock(
+      const_cast<base::Lock&>(previous_updater_status_lock_));
+  return previous_updater_status_;
+}
+
+void Configurator::SetPreviousUpdaterStatus(const std::string& status) {
+  base::AutoLock auto_lock(previous_updater_status_lock_);
+  previous_updater_status_ = status;
+}
+
 }  // namespace updater
 }  // namespace cobalt
diff --git a/src/cobalt/updater/configurator.h b/src/cobalt/updater/configurator.h
index e335b4f..cfd1488 100644
--- a/src/cobalt/updater/configurator.h
+++ b/src/cobalt/updater/configurator.h
@@ -81,9 +81,12 @@
 
   bool IsChannelValid(const std::string& channel);
 
-  std::string GetUpdaterStatus() const;
+  std::string GetUpdaterStatus() const override;
   void SetUpdaterStatus(const std::string& status);
 
+  std::string GetPreviousUpdaterStatus() const override;
+  void SetPreviousUpdaterStatus(const std::string& status) override;
+
  private:
   friend class base::RefCountedThreadSafe<Configurator>;
   ~Configurator() override;
@@ -98,6 +101,8 @@
   SbAtomic32 is_channel_changed_;
   std::string updater_status_;
   base::Lock updater_status_lock_;
+  std::string previous_updater_status_;
+  base::Lock previous_updater_status_lock_;
 
   DISALLOW_COPY_AND_ASSIGN(Configurator);
 };
diff --git a/src/cobalt/updater/noop_sandbox.cc b/src/cobalt/updater/noop_sandbox.cc
index d9ce028..5f63965 100644
--- a/src/cobalt/updater/noop_sandbox.cc
+++ b/src/cobalt/updater/noop_sandbox.cc
@@ -15,7 +15,12 @@
 // This is a test app for Evergreen that does nothing.
 
 #include "starboard/event.h"
+#include "starboard/system.h"
+#include "starboard/thread.h"
+#include "starboard/time.h"
 
 void SbEventHandle(const SbEvent* event) {
-  // noop
+  // No-op app. Exit after 1s.
+  SbThreadSleep(kSbTimeSecond);
+  SbSystemRequestStop(0);
 }
diff --git a/src/cobalt/updater/updater_module.cc b/src/cobalt/updater/updater_module.cc
index 2f0452b..9cbbd52 100644
--- a/src/cobalt/updater/updater_module.cc
+++ b/src/cobalt/updater/updater_module.cc
@@ -50,8 +50,8 @@
     0x9e, 0x8b, 0x2d, 0x22, 0x65, 0x19, 0xb1, 0xfa, 0xba, 0x02, 0x04,
     0x3a, 0xb2, 0x7a, 0xf6, 0xfe, 0xd5, 0x35, 0xa1, 0x19, 0xd9};
 
-// The map to translate updater status from enum to readable string.
-const std::map<ComponentState, const char*> updater_status_map = {
+// The map to translate update state from ComponentState to readable string.
+const std::map<ComponentState, const char*> update_state_map = {
     {ComponentState::kNew, "Will check for update soon"},
     {ComponentState::kChecking, "Checking for update"},
     {ComponentState::kCanUpdate, "Update is available"},
@@ -78,9 +78,15 @@
 void Observer::OnEvent(Events event, const std::string& id) {
   std::string status;
   if (update_client_->GetCrxUpdateState(id, &crx_update_item_)) {
-    auto status_iterator = updater_status_map.find(crx_update_item_.state);
-    if (status_iterator == updater_status_map.end()) {
+    auto status_iterator = update_state_map.find(crx_update_item_.state);
+    if (status_iterator == update_state_map.end()) {
       status = "Status is unknown.";
+    } else if (crx_update_item_.state == ComponentState::kUpToDate &&
+               updater_configurator_->GetPreviousUpdaterStatus().compare(
+                   update_state_map.find(ComponentState::kUpdated)->second) ==
+                   0) {
+      status =
+          std::string(update_state_map.find(ComponentState::kUpdated)->second);
     } else {
       status = std::string(status_iterator->second);
     }
diff --git a/src/cobalt/version.h b/src/cobalt/version.h
index 08526a7..acdcdfa 100644
--- a/src/cobalt/version.h
+++ b/src/cobalt/version.h
@@ -35,6 +35,6 @@
 //                  release is cut.
 //.
 
-#define COBALT_VERSION "21.lts.1"
+#define COBALT_VERSION "21.lts.2"
 
 #endif  // COBALT_VERSION_H_
diff --git a/src/cobalt/xhr/url_fetcher_buffer_writer.cc b/src/cobalt/xhr/url_fetcher_buffer_writer.cc
index 3b43b8a..fd67bb4 100644
--- a/src/cobalt/xhr/url_fetcher_buffer_writer.cc
+++ b/src/cobalt/xhr/url_fetcher_buffer_writer.cc
@@ -26,6 +26,9 @@
 // Allocate 64KB if the total size is unknown to avoid allocating small buffer
 // too many times.
 const int64_t kDefaultPreAllocateSizeInBytes = 64 * 1024;
+// Set max allocate size to avoid erroneous size estimate.
+const int64_t kMaxPreAllocateSizeInBytes = 10 * 1024 * 1024;
+const uint8_t kResizingMultiplier = 2;
 
 void ReleaseMemory(std::string* str) {
   DCHECK(str);
@@ -158,9 +161,16 @@
 
   if (capacity < 0) {
     capacity = kDefaultPreAllocateSizeInBytes;
+  } else if (capacity > kMaxPreAllocateSizeInBytes) {
+    LOG(WARNING) << "Allocation of " << capacity << " bytes is capped to "
+                 << kMaxPreAllocateSizeInBytes;
+    capacity = kMaxPreAllocateSizeInBytes;
   } else {
     capacity_known_ = true;
   }
+  // Record the desired_capacity_ to avoid reserving unused memory during
+  // resizing.
+  desired_capacity_ = static_cast<size_t>(capacity);
 
   if (capacity == 0) {
     return;
@@ -212,7 +222,16 @@
       SB_LOG(WARNING) << "Data written is larger than the preset capacity "
                       << data_as_array_buffer_.byte_length();
     }
-    data_as_array_buffer_.Resize(data_as_array_buffer_size_ + num_bytes);
+    size_t new_size = std::max(
+        std::min(data_as_array_buffer_.byte_length() * kResizingMultiplier,
+                 desired_capacity_),
+        data_as_array_buffer_size_ + num_bytes);
+    if (new_size > desired_capacity_) {
+      // Content-length is wrong, response size is completely unknown.
+      // Double the capacity to avoid frequent resizing.
+      new_size *= kResizingMultiplier;
+    }
+    data_as_array_buffer_.Resize(new_size);
   }
 
   auto destination = static_cast<uint8_t*>(data_as_array_buffer_.data()) +
diff --git a/src/cobalt/xhr/url_fetcher_buffer_writer.h b/src/cobalt/xhr/url_fetcher_buffer_writer.h
index 8eb9736..35058ab 100644
--- a/src/cobalt/xhr/url_fetcher_buffer_writer.h
+++ b/src/cobalt/xhr/url_fetcher_buffer_writer.h
@@ -74,6 +74,7 @@
     Type type_;
     bool allow_preallocate_ = true;
     bool capacity_known_ = false;
+    size_t desired_capacity_ = 0;
 
     // This class can be accessed by both network and MainWebModule threads.
     mutable base::Lock lock_;
diff --git a/src/cobalt/xhr/xhr.gyp b/src/cobalt/xhr/xhr.gyp
index 051822e..708f710 100644
--- a/src/cobalt/xhr/xhr.gyp
+++ b/src/cobalt/xhr/xhr.gyp
@@ -23,8 +23,6 @@
       'sources': [
         'url_fetcher_buffer_writer.cc',
         'url_fetcher_buffer_writer.h',
-        'xhr_response_data.cc',
-        'xhr_response_data.h',
         'xml_http_request.cc',
         'xml_http_request.h',
         'xml_http_request_event_target.cc',
@@ -58,7 +56,6 @@
       'target_name': 'xhr_test',
       'type': '<(gtest_target_type)',
       'sources': [
-        'xhr_response_data_test.cc',
         'xml_http_request_test.cc',
       ],
       'dependencies': [
diff --git a/src/cobalt/xhr/xhr_response_data.cc b/src/cobalt/xhr/xhr_response_data.cc
deleted file mode 100644
index d2e5051..0000000
--- a/src/cobalt/xhr/xhr_response_data.cc
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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 "cobalt/xhr/xhr_response_data.h"
-
-#include <algorithm>
-
-#include "cobalt/dom/global_stats.h"
-
-namespace cobalt {
-namespace xhr {
-
-namespace {
-
-// When we don't have any data, we still want to return a non-null pointer to a
-// valid memory location.  Because even it will never be accessed, a null
-// pointer may trigger undefined behavior in functions like memcpy.  So we
-// create this dummy value here and return its address when we don't have any
-// data.
-uint8 s_dummy;
-
-// We are using std::string to store binary data so we want to ensure that char
-// occupies one byte.
-COMPILE_ASSERT(sizeof(char) == 1, char_should_occupy_one_byte);
-
-}  // namespace
-
-XhrResponseData::XhrResponseData() { IncreaseMemoryUsage(); }
-
-XhrResponseData::~XhrResponseData() { DecreaseMemoryUsage(); }
-
-void XhrResponseData::Clear() {
-  DecreaseMemoryUsage();
-  // Use swap to force free the memory allocated.
-  std::string dummy;
-  data_.swap(dummy);
-  IncreaseMemoryUsage();
-}
-
-void XhrResponseData::Reserve(size_t new_capacity_bytes) {
-  DecreaseMemoryUsage();
-  data_.reserve(new_capacity_bytes);
-  IncreaseMemoryUsage();
-}
-
-void XhrResponseData::Append(const uint8* source_data, size_t size_bytes) {
-  if (size_bytes == 0) {
-    return;
-  }
-  DecreaseMemoryUsage();
-  data_.resize(data_.size() + size_bytes);
-  memcpy(&data_[data_.size() - size_bytes], source_data, size_bytes);
-  IncreaseMemoryUsage();
-}
-
-const uint8* XhrResponseData::data() const {
-  return data_.empty() ? &s_dummy : reinterpret_cast<const uint8*>(&data_[0]);
-}
-
-uint8* XhrResponseData::data() {
-  return data_.empty() ? &s_dummy : reinterpret_cast<uint8*>(&data_[0]);
-}
-
-void XhrResponseData::IncreaseMemoryUsage() {
-  dom::GlobalStats::GetInstance()->IncreaseXHRMemoryUsage(capacity());
-}
-
-void XhrResponseData::DecreaseMemoryUsage() {
-  dom::GlobalStats::GetInstance()->DecreaseXHRMemoryUsage(capacity());
-}
-
-}  // namespace xhr
-}  // namespace cobalt
diff --git a/src/cobalt/xhr/xhr_response_data.h b/src/cobalt/xhr/xhr_response_data.h
deleted file mode 100644
index 894cd3f..0000000
--- a/src/cobalt/xhr/xhr_response_data.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.
-
-#ifndef COBALT_XHR_XHR_RESPONSE_DATA_H_
-#define COBALT_XHR_XHR_RESPONSE_DATA_H_
-
-#include <string>
-
-#include "base/basictypes.h"
-
-namespace cobalt {
-namespace xhr {
-
-// Simple wrapper for an array of data.
-// Used by XMLHttpRequest to construct the response body.
-class XhrResponseData {
- public:
-  XhrResponseData();
-  ~XhrResponseData();
-
-  // Destroy the data_ and reset the size and capacity to 0.
-  void Clear();
-  // Allocate storage for |new_capacity_bytes| of data.
-  void Reserve(size_t new_capacity_bytes);
-  // Append |source_data|, |size_bytes| in length, to the data array.
-  void Append(const uint8* source_data, size_t size_bytes);
-
-  const uint8* data() const;
-  uint8* data();
-
-  const std::string& string() const { return data_; }
-
-  size_t size() const { return data_.size(); }
-  size_t capacity() const { return data_.capacity(); }
-
- private:
-  void IncreaseMemoryUsage();
-  void DecreaseMemoryUsage();
-
-  std::string data_;
-};
-
-}  // namespace xhr
-}  // namespace cobalt
-
-#endif  // COBALT_XHR_XHR_RESPONSE_DATA_H_
diff --git a/src/cobalt/xhr/xhr_response_data_test.cc b/src/cobalt/xhr/xhr_response_data_test.cc
deleted file mode 100644
index f63d70e..0000000
--- a/src/cobalt/xhr/xhr_response_data_test.cc
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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 "cobalt/xhr/xhr_response_data.h"
-
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace cobalt {
-namespace xhr {
-
-namespace {
-
-TEST(XhrResponseData, InitialState) {
-  XhrResponseData empty;
-  EXPECT_EQ(0u, empty.size());
-  EXPECT_TRUE(empty.data() != NULL);
-}
-
-TEST(XhrResponseData, Append) {
-  XhrResponseData data;
-  uint8 raw_data[64];
-  for (int i = 0; i < 64; ++i) {
-    raw_data[i] = static_cast<uint8>(i);
-  }
-  data.Append(raw_data, 64);
-  EXPECT_EQ(64u, data.size());
-  EXPECT_LE(64u, data.capacity());
-
-  for (int i = 0; i < 64; ++i) {
-    EXPECT_EQ(raw_data[i], data.data()[i]);
-  }
-}
-
-TEST(XhrResponseData, Reserve) {
-  XhrResponseData data;
-  data.Reserve(1);
-  EXPECT_LE(1u, data.capacity());
-  EXPECT_EQ(0u, data.size());
-  EXPECT_TRUE(data.data() != NULL);
-}
-
-}  // namespace
-}  // namespace xhr
-}  // namespace cobalt
diff --git a/src/components/update_client/component.cc b/src/components/update_client/component.cc
index 50ec274..9be0c39 100644
--- a/src/components/update_client/component.cc
+++ b/src/components/update_client/component.cc
@@ -559,6 +559,12 @@
   DCHECK(thread_checker_.CalledOnValidThread());
 
   auto& component = State::component();
+
+#if defined(OS_STARBOARD)
+  auto& config = component.update_context_.config;
+  config->SetPreviousUpdaterStatus(config->GetUpdaterStatus());
+#endif
+
   if (component.crx_component()) {
     TransitionState(std::make_unique<StateChecking>(&component));
   } else {
diff --git a/src/components/update_client/configurator.h b/src/components/update_client/configurator.h
index ab7e01e..64d931a 100644
--- a/src/components/update_client/configurator.h
+++ b/src/components/update_client/configurator.h
@@ -171,6 +171,11 @@
   // parameters.
   virtual void SetChannel(const std::string& channel) = 0;
 
+  virtual std::string GetPreviousUpdaterStatus() const = 0;
+  virtual void SetPreviousUpdaterStatus(const std::string& status) = 0;
+
+  virtual std::string GetUpdaterStatus() const = 0;
+
   // Compare and swap the is_channel_changed flag.
   virtual void CompareAndSwapChannelChanged(int old_value, int new_value) = 0;
 
diff --git a/src/starboard/android/apk/app/build.gradle b/src/starboard/android/apk/app/build.gradle
index 5ab0feb..8e527e2 100644
--- a/src/starboard/android/apk/app/build.gradle
+++ b/src/starboard/android/apk/app/build.gradle
@@ -105,8 +105,6 @@
             signingConfig signingConfigs.debugConfig
         }
         qa {
-            debuggable true
-            jniDebuggable true
             externalNativeBuild {
                 cmake.arguments "-DCOBALT_CONFIG=qa"
             }
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
index 9a609c0..cb9948d 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
@@ -27,6 +27,7 @@
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewParent;
 import android.widget.FrameLayout;
+import dev.cobalt.media.MediaCodecUtil;
 import dev.cobalt.media.VideoSurfaceView;
 import dev.cobalt.util.Log;
 import dev.cobalt.util.UsedByNative;
@@ -45,7 +46,9 @@
   private static final java.lang.String META_DATA_APP_URL = "cobalt.APP_URL";
 
   private static final String SPLASH_URL_ARG = "--fallback_splash_screen_url=";
+  private static final String SPLASH_TOPICS_ARG = "--fallback_splash_screen_topics=";
   private static final java.lang.String META_DATA_SPLASH_URL = "cobalt.SPLASH_URL";
+  private static final java.lang.String META_DATA_SPLASH_TOPICS = "cobalt.SPLASH_TOPIC";
 
   private static final String FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
       "--force_migration_for_storage_partitioning";
@@ -107,6 +110,9 @@
 
   @Override
   protected void onStart() {
+    if (!isReleaseBuild()) {
+      MediaCodecUtil.dumpAllDecoders();
+    }
     if (forceCreateNewVideoSurfaceView) {
       Log.w(TAG, "Force to create a new video surface.");
       createNewSurfaceView();
@@ -172,7 +178,9 @@
     boolean hasUrlArg = hasArg(args, URL_ARG);
     // If the splash screen url arg isn't specified, get it from AndroidManifest.xml.
     boolean hasSplashUrlArg = hasArg(args, SPLASH_URL_ARG);
-    if (!hasUrlArg || !hasSplashUrlArg) {
+    // If the splash screen topics arg isn't specified, get it from AndroidManifest.xml.
+    boolean hasSplashTopicsArg = hasArg(args, SPLASH_TOPICS_ARG);
+    if (!hasUrlArg || !hasSplashUrlArg || !hasSplashTopicsArg) {
       try {
         ActivityInfo ai =
             getPackageManager()
@@ -190,6 +198,12 @@
               args.add(SPLASH_URL_ARG + splashUrl);
             }
           }
+          if (!hasSplashTopicsArg) {
+            String splashTopics = ai.metaData.getString(META_DATA_SPLASH_TOPICS);
+            if (splashTopics != null) {
+              args.add(SPLASH_TOPICS_ARG + splashTopics);
+            }
+          }
           if (ai.metaData.getBoolean(META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING)) {
             args.add(FORCE_MIGRATION_FOR_STORAGE_PARTITIONING);
           }
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
index 06cc02f..799aef9 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/coat/VoiceRecognizer.java
@@ -122,7 +122,8 @@
     this.nativeSpeechRecognizerImpl = nativeSpeechRecognizer;
 
     if (this.audioPermissionRequester.requestRecordAudioPermission(
-        this.nativeSpeechRecognizerImpl)) {
+        this.nativeSpeechRecognizerImpl) &&
+        SpeechRecognizer.isRecognitionAvailable(context)) {
       startRecognitionInternal();
     } else {
       mainHandler.post(
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
index 554d8d5..b80fa03 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java
@@ -48,9 +48,18 @@
   @SuppressWarnings("unused")
   @UsedByNative
   AudioTrackBridge createAudioTrackBridge(
-      int sampleType, int sampleRate, int channelCount, int preferredBufferSizeInBytes) {
+      int sampleType,
+      int sampleRate,
+      int channelCount,
+      int preferredBufferSizeInBytes,
+      int tunnelModeAudioSessionId) {
     AudioTrackBridge audioTrackBridge =
-        new AudioTrackBridge(sampleType, sampleRate, channelCount, preferredBufferSizeInBytes);
+        new AudioTrackBridge(
+            sampleType,
+            sampleRate,
+            channelCount,
+            preferredBufferSizeInBytes,
+            tunnelModeAudioSessionId);
     if (!audioTrackBridge.isAudioTrackValid()) {
       Log.e(TAG, "AudioTrackBridge has invalid audio track");
       return null;
@@ -128,4 +137,29 @@
     }
     return AudioTrack.getMinBufferSize(sampleRate, channelConfig, sampleType);
   }
+
+  /** Generate audio session id used by tunneled playback. */
+  @SuppressWarnings("unused")
+  @UsedByNative
+  int generateTunnelModeAudioSessionId(int numberOfChannels) {
+    // Android 9.0 (Build.VERSION.SDK_INT >= 28) support v2 sync header that
+    // aligns sync header with audio frame size. V1 sync header has alignment
+    // issues for multi-channel audio.
+    if (Build.VERSION.SDK_INT < 28) {
+      // Currently we only support int16 under tunnel mode.
+      final int sampleSizeInBytes = 2;
+      final int frameSizeInBytes = sampleSizeInBytes * numberOfChannels;
+      if (AudioTrackBridge.AV_SYNC_HEADER_V1_SIZE % frameSizeInBytes != 0) {
+        Log.w(
+            TAG,
+            String.format(
+                "Disable tunnel mode due to sampleSizeInBytes (%d) * numberOfChannels (%d) isn't"
+                    + " aligned to AV_SYNC_HEADER_V1_SIZE (%d).",
+                sampleSizeInBytes, numberOfChannels, AudioTrackBridge.AV_SYNC_HEADER_V1_SIZE));
+        return -1;
+      }
+    }
+    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    return audioManager.generateAudioSessionId();
+  }
 }
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
index 419f5d8..b8d5a53 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -26,18 +26,45 @@
 import dev.cobalt.util.Log;
 import dev.cobalt.util.UsedByNative;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
-// A wrapper of the android AudioTrack class.
-// Android AudioTrack would not start playing until the buffer is fully
-// filled once.
+/**
+ * A wrapper of the android AudioTrack class. Android AudioTrack would not start playing until the
+ * buffer is fully filled once.
+ */
 @UsedByNative
 public class AudioTrackBridge {
+  // Also used by AudioOutputManager.
+  static final int AV_SYNC_HEADER_V1_SIZE = 16;
+
   private AudioTrack audioTrack;
   private AudioTimestamp audioTimestamp = new AudioTimestamp();
   private long maxFramePositionSoFar = 0;
 
+  private final boolean tunnelModeEnabled;
+  // The following variables are used only when |tunnelModeEnabled| is true.
+  private ByteBuffer avSyncHeader;
+  private int avSyncPacketBytesRemaining;
+
+  private static int getBytesPerSample(int audioFormat) {
+    switch (audioFormat) {
+      case AudioFormat.ENCODING_PCM_16BIT:
+        return 2;
+      case AudioFormat.ENCODING_PCM_FLOAT:
+        return 4;
+      case AudioFormat.ENCODING_INVALID:
+      default:
+        throw new RuntimeException("Unsupport audio format " + audioFormat);
+    }
+  }
+
   public AudioTrackBridge(
-      int sampleType, int sampleRate, int channelCount, int preferredBufferSizeInBytes) {
+      int sampleType,
+      int sampleRate,
+      int channelCount,
+      int preferredBufferSizeInBytes,
+      int tunnelModeAudioSessionId) {
+    tunnelModeEnabled = tunnelModeAudioSessionId != -1;
     int channelConfig;
     switch (channelCount) {
       case 1:
@@ -53,11 +80,40 @@
         throw new RuntimeException("Unsupported channel count: " + channelCount);
     }
 
-    AudioAttributes attributes =
-        new AudioAttributes.Builder()
-            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
-            .setUsage(AudioAttributes.USAGE_MEDIA)
-            .build();
+    AudioAttributes attributes;
+    if (tunnelModeEnabled) {
+      // Android 9.0 (Build.VERSION.SDK_INT >= 28) support v2 sync header that aligns sync header
+      // with audio frame size. V1 sync header has alignment issues for multi-channel audio.
+      if (Build.VERSION.SDK_INT < 28) {
+        int frameSize = getBytesPerSample(sampleType) * channelCount;
+        // This shouldn't happen as it should have been checked in
+        // AudioOutputManager.generateTunnelModeAudioSessionId().
+        if (AV_SYNC_HEADER_V1_SIZE % frameSize != 0) {
+          audioTrack = null;
+          String errorMessage =
+              String.format(
+                  "Enable tunnel mode when frame size is unaligned, "
+                      + "sampleType: %d, channel: %d, sync header size: %d.",
+                  sampleType, channelCount, AV_SYNC_HEADER_V1_SIZE);
+          Log.e(TAG, errorMessage);
+          throw new RuntimeException(errorMessage);
+        }
+      }
+      attributes =
+          new AudioAttributes.Builder()
+              .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+              .setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
+              .setUsage(AudioAttributes.USAGE_MEDIA)
+              .build();
+    } else {
+      // TODO: Investigate if we can use |CONTENT_TYPE_MOVIE| for AudioTrack
+      //       used by video playback.
+      attributes =
+          new AudioAttributes.Builder()
+              .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+              .setUsage(AudioAttributes.USAGE_MEDIA)
+              .build();
+    }
     AudioFormat format =
         new AudioFormat.Builder()
             .setEncoding(sampleType)
@@ -66,6 +122,10 @@
             .build();
 
     int audioTrackBufferSize = preferredBufferSizeInBytes;
+    // TODO: Investigate if this implementation could be refined.
+    // It is not necessary to loop until 0 since there is new implementation based on
+    // AudioTrack.getMinBufferSize(). Especially for tunnel mode, it would fail if audio HAL does
+    // not support tunnel mode and then it is not helpful to retry.
     while (audioTrackBufferSize > 0) {
       try {
         audioTrack =
@@ -74,7 +134,9 @@
                 format,
                 audioTrackBufferSize,
                 AudioTrack.MODE_STREAM,
-                AudioManager.AUDIO_SESSION_ID_GENERATE);
+                tunnelModeEnabled
+                    ? tunnelModeAudioSessionId
+                    : AudioManager.AUDIO_SESSION_ID_GENERATE);
       } catch (Exception e) {
         audioTrack = null;
       }
@@ -104,6 +166,8 @@
       audioTrack.release();
     }
     audioTrack = null;
+    avSyncHeader = null;
+    avSyncPacketBytesRemaining = 0;
   }
 
   @SuppressWarnings("unused")
@@ -144,15 +208,22 @@
       return;
     }
     audioTrack.flush();
+    avSyncHeader = null;
+    avSyncPacketBytesRemaining = 0;
   }
 
   @SuppressWarnings("unused")
   @UsedByNative
-  private int write(byte[] audioData, int sizeInBytes) {
+  private int write(byte[] audioData, int sizeInBytes, long presentationTimeInMicroseconds) {
     if (audioTrack == null) {
       Log.e(TAG, "Unable to write with NULL audio track.");
       return 0;
     }
+
+    if (tunnelModeEnabled) {
+      return writeWithAvSync(audioData, sizeInBytes, presentationTimeInMicroseconds);
+    }
+
     if (Build.VERSION.SDK_INT >= 23) {
       return audioTrack.write(audioData, 0, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING);
     } else {
@@ -161,6 +232,66 @@
     }
   }
 
+  private int writeWithAvSync(
+      byte[] audioData, int sizeInBytes, long presentationTimeInMicroseconds) {
+    if (audioTrack == null) {
+      throw new RuntimeException("writeWithAvSync() is called when audioTrack is null.");
+    }
+
+    if (!tunnelModeEnabled) {
+      throw new RuntimeException("writeWithAvSync() is called when tunnelModeEnabled is false.");
+    }
+
+    long presentationTimeInNanoseconds = presentationTimeInMicroseconds * 1000;
+
+    // Android support tunnel mode from 5.0 (API level 21), but the app has to manually write the
+    // sync header before API 23, where the write() function with presentation timestamp is
+    // introduced.
+    // Set the following constant to |false| to test manual sync header writing in API level 23 or
+    // later.  Note that the code to write sync header manually only supports v1 sync header.
+    final boolean useAutoSyncHeaderWrite = false;
+    if (useAutoSyncHeaderWrite && Build.VERSION.SDK_INT >= 23) {
+      ByteBuffer byteBuffer = ByteBuffer.wrap(audioData);
+      return audioTrack.write(
+          byteBuffer, sizeInBytes, AudioTrack.WRITE_NON_BLOCKING, presentationTimeInNanoseconds);
+    }
+
+    if (avSyncHeader == null) {
+      avSyncHeader = ByteBuffer.allocate(AV_SYNC_HEADER_V1_SIZE);
+      avSyncHeader.order(ByteOrder.BIG_ENDIAN);
+      avSyncHeader.putInt(0x55550001);
+    }
+
+    if (avSyncPacketBytesRemaining == 0) {
+      avSyncHeader.putInt(4, sizeInBytes);
+      avSyncHeader.putLong(8, presentationTimeInNanoseconds);
+      avSyncHeader.position(0);
+      avSyncPacketBytesRemaining = sizeInBytes;
+    }
+
+    if (avSyncHeader.remaining() > 0) {
+      int ret =
+          audioTrack.write(avSyncHeader, avSyncHeader.remaining(), AudioTrack.WRITE_NON_BLOCKING);
+      if (ret < 0) {
+        avSyncPacketBytesRemaining = 0;
+        return ret;
+      }
+      if (avSyncHeader.remaining() > 0) {
+        return 0;
+      }
+    }
+
+    int sizeToWrite = Math.min(avSyncPacketBytesRemaining, sizeInBytes);
+    ByteBuffer byteBuffer = ByteBuffer.wrap(audioData);
+    int ret = audioTrack.write(byteBuffer, sizeToWrite, AudioTrack.WRITE_NON_BLOCKING);
+    if (ret < 0) {
+      avSyncPacketBytesRemaining = 0;
+      return ret;
+    }
+    avSyncPacketBytesRemaining -= ret;
+    return ret;
+  }
+
   @SuppressWarnings("unused")
   @UsedByNative
   private int write(float[] audioData, int sizeInFloats) {
@@ -168,6 +299,9 @@
       Log.e(TAG, "Unable to write with NULL audio track.");
       return 0;
     }
+    if (tunnelModeEnabled) {
+      throw new RuntimeException("Float sample is not supported under tunnel mode.");
+    }
     return audioTrack.write(audioData, 0, sizeInFloats, AudioTrack.WRITE_NON_BLOCKING);
   }
 
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
index faa8742..d0585e9 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java
@@ -592,8 +592,25 @@
       mediaFormat.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);
     }
 
-    int maxWidth = findVideoDecoderResult.videoCapabilities.getSupportedWidths().getUpper();
-    int maxHeight = findVideoDecoderResult.videoCapabilities.getSupportedHeights().getUpper();
+    VideoCapabilities videoCapabilities = findVideoDecoderResult.videoCapabilities;
+    int maxWidth = videoCapabilities.getSupportedWidths().getUpper();
+    int maxHeight = videoCapabilities.getSupportedHeights().getUpper();
+    if (!videoCapabilities.isSizeSupported(maxWidth, maxHeight)) {
+      if (maxHeight >= 4320 && videoCapabilities.isSizeSupported(7680, 4320)) {
+        maxWidth = 7680;
+        maxHeight = 4320;
+      } else if (maxHeight >= 2160 && videoCapabilities.isSizeSupported(3840, 2160)) {
+        maxWidth = 3840;
+        maxHeight = 2160;
+      } else if (maxHeight >= 1080 && videoCapabilities.isSizeSupported(1920, 1080)) {
+        maxWidth = 1920;
+        maxHeight = 1080;
+      } else {
+        Log.e(TAG, "Failed to find a compatible resolution");
+        maxWidth = 1920;
+        maxHeight = 1080;
+      }
+    }
     if (!bridge.configureVideo(
         mediaFormat,
         surface,
@@ -915,8 +932,9 @@
         // adapt up to 8k at any point. We thus request 8k buffers up front,
         // unless the decoder claims to not be able to do 8k, in which case
         // we're ok, since we would've rejected a 8k stream when canPlayType
-        // was called, and then use those decoder values instead.
-        if (Build.VERSION.SDK_INT > 22) {
+        // was called, and then use those decoder values instead. We only
+        // support 8k for API level 29 and above.
+        if (Build.VERSION.SDK_INT > 28) {
           format.setInteger(MediaFormat.KEY_MAX_WIDTH, Math.min(7680, maxSupportedWidth));
           format.setInteger(MediaFormat.KEY_MAX_HEIGHT, Math.min(4320, maxSupportedHeight));
         } else {
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
index 40fcde0..03fb7cc 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java
@@ -573,21 +573,33 @@
         }
 
         VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
-        if (frameWidth != 0 && !videoCapabilities.getSupportedWidths().contains(frameWidth)) {
-          Log.v(
-              TAG,
-              String.format(
-                  "Rejecting %s, reason: supported widths %s does not contain %d",
-                  name, videoCapabilities.getSupportedWidths().toString(), frameWidth));
-          continue;
-        }
-        if (frameHeight != 0 && !videoCapabilities.getSupportedHeights().contains(frameHeight)) {
-          Log.v(
-              TAG,
-              String.format(
-                  "Rejecting %s, reason: supported heights %s does not contain %d",
-                  name, videoCapabilities.getSupportedHeights().toString(), frameHeight));
-          continue;
+        if (frameWidth != 0 && frameHeight != 0) {
+          if (!videoCapabilities.isSizeSupported(frameWidth, frameHeight)) {
+            Log.v(
+                TAG,
+                String.format(
+                    "Rejecting %s, reason: width %s is not compatible with height %d",
+                    name, frameWidth, frameHeight));
+            continue;
+          }
+        } else if (frameWidth != 0) {
+          if (!videoCapabilities.getSupportedWidths().contains(frameWidth)) {
+            Log.v(
+                TAG,
+                String.format(
+                    "Rejecting %s, reason: supported widths %s does not contain %d",
+                    name, videoCapabilities.getSupportedWidths().toString(), frameWidth));
+            continue;
+          }
+        } else if (frameHeight != 0) {
+          if (!videoCapabilities.getSupportedHeights().contains(frameHeight)) {
+            Log.v(
+                TAG,
+                String.format(
+                    "Rejecting %s, reason: supported heights %s does not contain %d",
+                    name, videoCapabilities.getSupportedHeights().toString(), frameHeight));
+            continue;
+          }
         }
         if (bitrate != 0 && !videoCapabilities.getBitrateRange().contains(bitrate)) {
           Log.v(
@@ -597,13 +609,31 @@
                   name, videoCapabilities.getBitrateRange().toString(), bitrate));
           continue;
         }
-        if (fps != 0 && !videoCapabilities.getSupportedFrameRates().contains(fps)) {
-          Log.v(
-              TAG,
-              String.format(
-                  "Rejecting %s, reason: supported frame rates %s does not contain %d",
-                  name, videoCapabilities.getSupportedFrameRates().toString(), fps));
-          continue;
+        if (fps != 0) {
+          if (frameHeight != 0 && frameWidth != 0) {
+            if (!videoCapabilities.areSizeAndRateSupported(frameWidth, frameHeight, fps)) {
+              Log.v(
+                  TAG,
+                  String.format(
+                      "Rejecting %s, reason: supported frame rates %s does not contain %d",
+                      name,
+                      videoCapabilities
+                          .getSupportedFrameRatesFor(frameWidth, frameHeight)
+                          .toString(),
+                      fps));
+              continue;
+            }
+          } else {
+            // At least one of frameHeight or frameWidth is 0
+            if (!videoCapabilities.getSupportedFrameRates().contains(fps)) {
+              Log.v(
+                  TAG,
+                  String.format(
+                      "Rejecting %s, reason: supported frame rates %s does not contain %d",
+                      name, videoCapabilities.getSupportedFrameRates().toString(), fps));
+              continue;
+            }
+          }
         }
         String resultName =
             (secure && !name.endsWith(SECURE_DECODER_SUFFIX))
@@ -652,52 +682,69 @@
    * Debug utility function that can be locally added to dump information about all decoders on a
    * particular system.
    */
-  @SuppressWarnings("unused")
-  private static void dumpAllDecoders() {
+  public static void dumpAllDecoders() {
+    String decoderDumpString = "";
     for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
       if (info.isEncoder()) {
         continue;
       }
       for (String supportedType : info.getSupportedTypes()) {
         String name = info.getName();
-        CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
-        Log.v(TAG, "==================================================");
-        Log.v(TAG, String.format("name: %s", name));
-        Log.v(TAG, String.format("supportedType: %s", supportedType));
-        Log.v(
-            TAG, String.format("codecBlackList.contains(name): %b", codecBlackList.contains(name)));
-        Log.v(
-            TAG,
+        decoderDumpString +=
             String.format(
-                "FEATURE_SecurePlayback: %b",
-                codecCapabilities.isFeatureSupported(
-                    MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback)));
+                "name: %s (%s, %s):",
+                name,
+                supportedType,
+                codecBlackList.contains(name) ? "blacklisted" : "not blacklisted");
+        CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType);
         VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
         if (videoCapabilities != null) {
-          Log.v(
-              TAG,
+          decoderDumpString +=
               String.format(
-                  "videoCapabilities.getSupportedWidths(): %s",
-                  videoCapabilities.getSupportedWidths().toString()));
-          Log.v(
-              TAG,
-              String.format(
-                  "videoCapabilities.getSupportedHeights(): %s",
-                  videoCapabilities.getSupportedHeights().toString()));
-          Log.v(
-              TAG,
-              String.format(
-                  "videoCapabilities.getBitrateRange(): %s",
-                  videoCapabilities.getBitrateRange().toString()));
-          Log.v(
-              TAG,
-              String.format(
-                  "videoCapabilities.getSupportedFrameRates(): %s",
-                  videoCapabilities.getSupportedFrameRates().toString()));
+                  "\n\t\t"
+                      + "widths: %s, "
+                      + "heights: %s, "
+                      + "bitrates: %s, "
+                      + "framerates: %s, ",
+                  videoCapabilities.getSupportedWidths().toString(),
+                  videoCapabilities.getSupportedHeights().toString(),
+                  videoCapabilities.getBitrateRange().toString(),
+                  videoCapabilities.getSupportedFrameRates().toString());
         }
-        Log.v(TAG, "==================================================");
-        Log.v(TAG, "");
+        boolean adaptivePlayback =
+            codecCapabilities.isFeatureSupported(
+                MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback);
+        boolean securePlayback =
+            codecCapabilities.isFeatureSupported(
+                MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
+        boolean tunneledPlayback =
+            codecCapabilities.isFeatureSupported(
+                MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback);
+        if (adaptivePlayback || securePlayback || tunneledPlayback) {
+          decoderDumpString +=
+              String.format(
+                  "(%s%s%s",
+                  adaptivePlayback ? "AdaptivePlayback, " : "",
+                  securePlayback ? "SecurePlayback, " : "",
+                  tunneledPlayback ? "TunneledPlayback, " : "");
+          // Remove trailing space and comma
+          decoderDumpString = decoderDumpString.substring(0, decoderDumpString.length() - 2);
+          decoderDumpString += ")";
+        } else {
+          decoderDumpString += " No extra features supported";
+        }
+        decoderDumpString += "\n";
       }
     }
+    Log.v(
+        TAG,
+        String.format(
+            " \n"
+                + "==================================================\n"
+                + "Full list of decoder features: [AdaptivePlayback, SecurePlayback,"
+                + " TunneledPlayback]\n"
+                + "Unsupported features for each codec are not listed\n"
+                + decoderDumpString
+                + "=================================================="));
   }
 }
diff --git a/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc b/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
index a5a77a7..88f3762 100644
--- a/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
+++ b/src/starboard/android/shared/audio_sink_min_required_frames_tester.cc
@@ -123,7 +123,8 @@
         min_required_frames_ * task.number_of_channels *
             GetSampleSize(task.sample_type),
         &MinRequiredFramesTester::UpdateSourceStatusFunc,
-        &MinRequiredFramesTester::ConsumeFramesFunc, this);
+        &MinRequiredFramesTester::ConsumeFramesFunc,
+        &MinRequiredFramesTester::ErrorFunc, 0, -1, this);
     {
       ScopedLock scoped_lock(mutex_);
       wait_timeout = !condition_variable_.WaitTimed(kSbTimeSecond * 5);
@@ -174,6 +175,14 @@
   tester->ConsumeFrames(frames_consumed);
 }
 
+// static
+void MinRequiredFramesTester::ErrorFunc(bool capability_changed,
+                                        void* context) {
+  // TODO: Handle errors during minimum frames test, maybe by terminating the
+  //       test earlier.
+  SB_NOTREACHED();
+}
+
 void MinRequiredFramesTester::UpdateSourceStatus(int* frames_in_buffer,
                                                  int* offset_in_frames,
                                                  bool* is_playing,
diff --git a/src/starboard/android/shared/audio_sink_min_required_frames_tester.h b/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
index 290011d..e689e96 100644
--- a/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
+++ b/src/starboard/android/shared/audio_sink_min_required_frames_tester.h
@@ -85,6 +85,7 @@
   static void ConsumeFramesFunc(int frames_consumed,
                                 SbTime frames_consumed_at,
                                 void* context);
+  static void ErrorFunc(bool capability_changed, void* context);
   void UpdateSourceStatus(int* frames_in_buffer,
                           int* offset_in_frames,
                           bool* is_playing,
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.cc b/src/starboard/android/shared/audio_track_audio_sink_type.cc
index 44dffc8..cf9d744 100644
--- a/src/starboard/android/shared/audio_track_audio_sink_type.cc
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.cc
@@ -29,11 +29,23 @@
 namespace shared {
 namespace {
 
+// The same as AudioTrack.ERROR_DEAD_OBJECT.
+const int kAudioTrackErrorDeadObject = -6;
+
 // 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;
+
+// Most Android audio HAL updates audio time for A/V synchronization on audio
+// sync frames. For example, audio HAL may try to render when it gets an entire
+// sync frame and then update audio time. Shorter duration of sync frame
+// improves the accuracy of audio time, especially at the beginning of a
+// playback, as otherwise the audio time during the initial update may be too
+// large (non zero) and results in dropped video frames.
+const SbTime kMaxDurationPerRequestInTunnelMode = 16 * kSbTimeMillisecond;
+
 const jint kNoOffset = 0;
 const size_t kSilenceFramesPerAppend = 1024;
 
@@ -74,6 +86,12 @@
   return static_cast<uint8_t*>(pointer) + offset;
 }
 
+int GetMaxFramesPerRequestForTunnelMode(int sampling_frequency_hz) {
+  auto max_frames = kMaxDurationPerRequestInTunnelMode * sampling_frequency_hz /
+                    kSbTimeSecond;
+  return (max_frames + 15) / 16 * 16;  // align to 16
+}
+
 }  // namespace
 
 AudioTrackAudioSink::AudioTrackAudioSink(
@@ -86,6 +104,9 @@
     int preferred_buffer_size_in_bytes,
     SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
     ConsumeFramesFunc consume_frames_func,
+    SbAudioSinkPrivate::ErrorFunc error_func,
+    SbTime start_time,
+    int tunnel_mode_audio_session_id,
     void* context)
     : type_(type),
       channels_(channels),
@@ -95,40 +116,56 @@
       frames_per_channel_(frames_per_channel),
       update_source_status_func_(update_source_status_func),
       consume_frames_func_(consume_frames_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) {
+      error_func_(error_func),
+      start_time_(start_time),
+      tunnel_mode_audio_session_id_(tunnel_mode_audio_session_id),
+      max_frames_per_request_(
+          tunnel_mode_audio_session_id_ == -1
+              ? kMaxFramesPerRequest
+              : GetMaxFramesPerRequestForTunnelMode(sampling_frequency_hz_)),
+      context_(context) {
   SB_DCHECK(update_source_status_func_);
   SB_DCHECK(consume_frames_func_);
   SB_DCHECK(frame_buffer_);
   SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(sample_type));
 
+  // TODO: Support query if platform supports float type for tunnel mode.
+  if (tunnel_mode_audio_session_id_ != -1) {
+    SB_DCHECK(sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated);
+  }
+
+  SB_LOG(INFO) << "Creating audio sink starts at " << start_time_;
+
   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;",
+      "(IIIII)Ldev/cobalt/media/AudioTrackBridge;",
       GetAudioFormatSampleType(sample_type_), sampling_frequency_hz_, channels_,
-      preferred_buffer_size_in_bytes);
+      preferred_buffer_size_in_bytes, tunnel_mode_audio_session_id_);
   if (!j_audio_track_bridge) {
+    // One of the cases that this may hit is when output happened to be switched
+    // to a device that doesn't support tunnel mode.
+    // TODO: Find a way to exclude the device from tunnel mode playback, to
+    //       avoid infinite loop in creating the audio sink on a device
+    //       claims to support tunnel mode but fails to create the audio sink.
+    // TODO: Currently this will be reported as a general decode error,
+    //       investigate if this can be reported as a capability changed error.
     return;
   }
   j_audio_track_bridge_ = env->ConvertLocalRefToGlobalRef(j_audio_track_bridge);
   if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
-    j_audio_data_ = env->NewFloatArray(channels_ * kMaxFramesPerRequest);
+    j_audio_data_ = env->NewFloatArray(channels_ * max_frames_per_request_);
   } else if (sample_type_ == kSbMediaAudioSampleTypeInt16Deprecated) {
     j_audio_data_ = env->NewByteArray(channels_ * GetSampleSize(sample_type_) *
-                                      kMaxFramesPerRequest);
+                                      max_frames_per_request_);
   } else {
     SB_NOTREACHED();
   }
+  SB_CHECK(j_audio_data_) << "Failed to allocate |j_audio_data_|";
+
   j_audio_data_ = env->ConvertLocalRefToGlobalRef(j_audio_data_);
 
   audio_out_thread_ = SbThreadCreate(
@@ -183,15 +220,21 @@
   return NULL;
 }
 
+// TODO: Break down the function into manageable pieces.
 void AudioTrackAudioSink::AudioThreadFunc() {
   JniEnvExt* env = JniEnvExt::Get();
   bool was_playing = false;
 
   SB_LOG(INFO) << "AudioTrackAudioSink thread started.";
 
-#if defined(SB_PLAYER_FILTER_ENABLE_STATE_CHECK)
+  int accumulated_written_frames = 0;
+  // TODO: |last_playback_head_changed_at| is also resetted when a warning is
+  //       logged after the playback head position hasn't been updated for a
+  //       while.  We should refine the name to make it better reflect its
+  //       usage.
   SbTime last_playback_head_changed_at = -1;
-#endif  // defined(SB_PLAYER_FILTER_ENABLE_STATE_CHECK)
+  SbTime playback_head_not_changed_duration = 0;
+  SbTime last_written_succeeded_at = -1;
 
   while (!quit_) {
     int playback_head_position = 0;
@@ -214,12 +257,15 @@
           std::max(playback_head_position, last_playback_head_position_);
       int frames_consumed =
           playback_head_position - last_playback_head_position_;
+      SbTime now = SbTimeGetMonotonicNow();
 
-#if defined(SB_PLAYER_FILTER_ENABLE_STATE_CHECK)
+      if (last_playback_head_changed_at == -1) {
+        last_playback_head_changed_at = now;
+      }
       if (last_playback_head_position_ == playback_head_position) {
-        auto now = SbTimeGetMonotonicNow();
         SbTime elapsed = now - last_playback_head_changed_at;
-        if (was_playing && elapsed > 5 * kSbTimeSecond) {
+        if (elapsed > 5 * kSbTimeSecond) {
+          playback_head_not_changed_duration += elapsed;
           last_playback_head_changed_at = now;
           SB_LOG(INFO) << "last playback head position is "
                        << last_playback_head_position_
@@ -227,9 +273,9 @@
                        << " microseconds.";
         }
       } else {
-        last_playback_head_changed_at = SbTimeGetMonotonicNow();
+        last_playback_head_changed_at = now;
+        playback_head_not_changed_duration = 0;
       }
-#endif  // defined(SB_PLAYER_FILTER_ENABLE_STATE_CHECK)
 
       last_playback_head_position_ = playback_head_position;
       frames_consumed = std::min(frames_consumed, written_frames_);
@@ -260,6 +306,9 @@
       SB_LOG(INFO) << "AudioTrackAudioSink paused.";
     } else if (!was_playing && is_playing) {
       was_playing = true;
+      last_playback_head_changed_at = -1;
+      playback_head_not_changed_duration = 0;
+      last_written_succeeded_at = -1;
       env->CallVoidMethodOrAbort(j_audio_track_bridge_, "play", "()V");
       SB_LOG(INFO) << "AudioTrackAudioSink playing.";
     }
@@ -281,7 +330,7 @@
     }
 
     expected_written_frames =
-        std::min(expected_written_frames, kMaxFramesPerRequest);
+        std::min(expected_written_frames, max_frames_per_request_);
     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. If eos is reached,
@@ -292,24 +341,84 @@
         // Currently AudioDevice and AudioRenderer will write tail silence.
         // It should be reached only in tests. It's not ideal to allocate
         // a new silence buffer every time.
-        std::vector<uint8_t> silence_buffer(
-            channels_ * GetSampleSize(sample_type_) * kSilenceFramesPerAppend);
-        WriteData(env, silence_buffer.data(), kSilenceFramesPerAppend);
+        const int silence_frames_per_append =
+            std::min<int>(kSilenceFramesPerAppend, max_frames_per_request_);
+        std::vector<uint8_t> silence_buffer(channels_ *
+                                            GetSampleSize(sample_type_) *
+                                            silence_frames_per_append);
+        auto sync_time = start_time_ + accumulated_written_frames *
+                                           kSbTimeSecond /
+                                           sampling_frequency_hz_;
+        // Not necessary to handle error of WriteData(), even for
+        // kAudioTrackErrorDeadObject, as the audio has reached the end of
+        // stream.
+        // TODO: Ensure that the audio stream can still reach the end when an
+        //       error occurs.
+        WriteData(env, silence_buffer.data(), silence_frames_per_append,
+                  sync_time);
       }
+
+      // While WriteData() returns kAudioTrackErrorDeadObject on dead object,
+      // getAudioTimestamp() doesn't, it just no longer updates its return
+      // value.  If the dead object error occurs when there isn't any audio data
+      // to write, there is no way to detect it by checking the return value of
+      // getAudioTimestamp().  Instead, we have to check if its return value has
+      // been changed during a certain period of time, to detect the underlying
+      // dead object error.
+      // Note that dead object would occur while switching audio end points in
+      // tunnel mode.  Under non-tunnel mode, the Android native AudioTrack will
+      // handle audio track dead object automatically if the new end point can
+      // support current audio format.
+      // TODO: Investigate to handle this error in non-tunnel mode.
+      if (tunnel_mode_audio_session_id_ != -1 &&
+          last_written_succeeded_at != -1) {
+        const SbTime kAudioSinkBlockedDuration = kSbTimeSecond;
+        auto time_since_last_written_success =
+            SbTimeGetMonotonicNow() - last_written_succeeded_at;
+        if (time_since_last_written_success > kAudioSinkBlockedDuration &&
+            playback_head_not_changed_duration > kAudioSinkBlockedDuration &&
+            tunnel_mode_audio_session_id_ != -1) {
+          SB_LOG(INFO) << "Over one second without frames written and consumed";
+          consume_frames_func_(written_frames_, SbTimeGetMonotonicNow(),
+                               context_);
+          error_func_(kSbPlayerErrorCapabilityChanged, context_);
+          break;
+        }
+      }
+
       SbThreadSleep(10 * kSbTimeMillisecond);
       continue;
     }
     SB_DCHECK(expected_written_frames > 0);
+    auto sync_time = start_time_ + accumulated_written_frames * kSbTimeSecond /
+                                       sampling_frequency_hz_;
+
+    SB_CHECK(start_position + expected_written_frames <= frames_per_channel_);
     int written_frames = WriteData(
         env,
         IncrementPointerByBytes(frame_buffer_, start_position * channels_ *
                                                    GetSampleSize(sample_type_)),
-        expected_written_frames);
+        expected_written_frames, sync_time);
+    SbTime now = SbTimeGetMonotonicNow();
+
+    if (written_frames < 0) {
+      // Take all |written_frames_| as consumed since audio track could be dead.
+      consume_frames_func_(written_frames_, now, context_);
+      error_func_(written_frames == kAudioTrackErrorDeadObject
+                      ? kSbPlayerErrorCapabilityChanged
+                      : kSbPlayerErrorDecode,
+                  context_);
+      break;
+    } else if (written_frames > 0) {
+      last_written_succeeded_at = now;
+    }
     written_frames_ += written_frames;
+    accumulated_written_frames += written_frames;
+
     bool written_fully = (written_frames == expected_written_frames);
     auto unplayed_frames_in_time =
         written_frames_ * kSbTimeSecond / sampling_frequency_hz_ -
-        (SbTimeGetMonotonicNow() - frames_consumed_at);
+        (now - 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
@@ -332,8 +441,9 @@
 }
 
 int AudioTrackAudioSink::WriteData(JniEnvExt* env,
-                                   const void* buffer,
-                                   int expected_written_frames) {
+                                   void* buffer,
+                                   int expected_written_frames,
+                                   SbTime sync_time) {
   if (sample_type_ == kSbMediaAudioSampleTypeFloat32) {
     int expected_written_size = expected_written_frames * channels_;
     env->SetFloatArrayRegion(static_cast<jfloatArray>(j_audio_data_), kNoOffset,
@@ -342,7 +452,10 @@
     int written =
         env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([FI)I",
                                   j_audio_data_, expected_written_size);
-    SB_DCHECK(written >= 0);
+    if (written < 0) {
+      // Error code returned as negative value, like kAudioTrackErrorDeadObject.
+      return written;
+    }
     SB_DCHECK(written % channels_ == 0);
     return written / channels_;
   }
@@ -352,10 +465,14 @@
     env->SetByteArrayRegion(static_cast<jbyteArray>(j_audio_data_), kNoOffset,
                             expected_written_size,
                             static_cast<const jbyte*>(buffer));
-    int written =
-        env->CallIntMethodOrAbort(j_audio_track_bridge_, "write", "([BI)I",
-                                  j_audio_data_, expected_written_size);
-    SB_DCHECK(written >= 0);
+
+    int written = env->CallIntMethodOrAbort(j_audio_track_bridge_, "write",
+                                            "([BIJ)I", j_audio_data_,
+                                            expected_written_size, sync_time);
+    if (written < 0) {
+      // Error code returned as negative value, like kAudioTrackErrorDeadObject.
+      return written;
+    }
     SB_DCHECK(written % (channels_ * GetSampleSize(sample_type_)) == 0);
     return written / (channels_ * GetSampleSize(sample_type_));
   }
@@ -417,6 +534,27 @@
     SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
     SbAudioSinkPrivate::ErrorFunc error_func,
     void* context) {
+  const SbTime kStartTime = 0;
+  const int kTunnelModeAudioSessionId = -1;  // disable tunnel mode
+  return Create(channels, sampling_frequency_hz, audio_sample_type,
+                audio_frame_storage_type, frame_buffers, frames_per_channel,
+                update_source_status_func, consume_frames_func, error_func,
+                kStartTime, kTunnelModeAudioSessionId, context);
+}
+
+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,
+    SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
+    SbAudioSinkPrivate::ErrorFunc error_func,
+    SbTime start_media_time,
+    int tunnel_mode_audio_session_id,
+    void* context) {
   int min_required_frames = SbAudioSinkGetMinBufferSizeInFrames(
       channels, audio_sample_type, sampling_frequency_hz);
   SB_DCHECK(frames_per_channel >= min_required_frames);
@@ -425,7 +563,8 @@
   AudioTrackAudioSink* audio_sink = new AudioTrackAudioSink(
       this, channels, sampling_frequency_hz, audio_sample_type, frame_buffers,
       frames_per_channel, preferred_buffer_size_in_bytes,
-      update_source_status_func, consume_frames_func, context);
+      update_source_status_func, consume_frames_func, error_func,
+      start_media_time, tunnel_mode_audio_session_id, context);
   if (!audio_sink->IsAudioTrackValid()) {
     SB_DLOG(ERROR)
         << "AudioTrackAudioSinkType::Create failed to create audio track";
diff --git a/src/starboard/android/shared/audio_track_audio_sink_type.h b/src/starboard/android/shared/audio_track_audio_sink_type.h
index 43b0c86..c5b1ba7 100644
--- a/src/starboard/android/shared/audio_track_audio_sink_type.h
+++ b/src/starboard/android/shared/audio_track_audio_sink_type.h
@@ -18,6 +18,7 @@
 #include <atomic>
 #include <functional>
 #include <map>
+#include <vector>
 
 #include "starboard/android/shared/audio_sink_min_required_frames_tester.h"
 #include "starboard/android/shared/jni_env_ext.h"
@@ -54,6 +55,19 @@
       SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
       SbAudioSinkPrivate::ErrorFunc error_func,
       void* context) override;
+  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,
+      SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
+      SbAudioSinkPrivate::ErrorFunc error_func,
+      SbTime start_time,
+      int tunnel_mode_audio_session_id,
+      void* context);
 
   bool IsValid(SbAudioSink audio_sink) override {
     return audio_sink != kSbAudioSinkInvalid && audio_sink->IsType(this);
@@ -92,6 +106,9 @@
       int preferred_buffer_size,
       SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
       ConsumeFramesFunc consume_frames_func,
+      SbAudioSinkPrivate::ErrorFunc error_func,
+      SbTime start_media_time,
+      int tunnel_mode_audio_session_id,
       void* context);
   ~AudioTrackAudioSink() override;
 
@@ -106,8 +123,9 @@
   static void* ThreadEntryPoint(void* context);
   void AudioThreadFunc();
 
-  int WriteData(JniEnvExt* env, const void* buffer, int size);
+  int WriteData(JniEnvExt* env, void* buffer, int size, SbTime sync_time);
 
+  // TODO: Add const to the following variables where appropriate.
   Type* type_;
   int channels_;
   int sampling_frequency_hz_;
@@ -116,18 +134,25 @@
   int frames_per_channel_;
   SbAudioSinkUpdateSourceStatusFunc update_source_status_func_;
   ConsumeFramesFunc consume_frames_func_;
+  SbAudioSinkPrivate::ErrorFunc error_func_;
+  const SbTime start_time_;
+  const int tunnel_mode_audio_session_id_;
+  const int max_frames_per_request_;
+
   void* context_;
-  int last_playback_head_position_;
-  jobject j_audio_track_bridge_;
-  jobject j_audio_data_;
+  int last_playback_head_position_ = 0;
+  jobject j_audio_track_bridge_ = nullptr;
+  jobject j_audio_data_ = nullptr;
 
-  volatile bool quit_;
-  SbThread audio_out_thread_;
+  volatile bool quit_ = false;
+  SbThread audio_out_thread_ = kSbThreadInvalid;
 
-  starboard::Mutex mutex_;
-  double playback_rate_;
+  Mutex mutex_;
+  double playback_rate_ = 1.0;
 
-  int written_frames_;
+  // TODO: Rename to |frames_in_audio_track| and move it into AudioThreadFunc()
+  //       as a local variable.
+  int written_frames_ = 0;
 };
 
 }  // namespace shared
diff --git a/src/starboard/android/shared/cobalt/__init__.py b/src/starboard/android/shared/cobalt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/shared/cobalt/__init__.py
diff --git a/src/starboard/android/shared/configuration.cc b/src/starboard/android/shared/configuration.cc
index 6f05a18..0de9388 100644
--- a/src/starboard/android/shared/configuration.cc
+++ b/src/starboard/android/shared/configuration.cc
@@ -43,7 +43,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &CobaltUserOnExitStrategy,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &CobaltEglSwapInterval,
@@ -66,6 +66,7 @@
     &common::CobaltGcZealDefault,
     &common::CobaltRasterizerTypeDefault,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/android/shared/configuration_public.h b/src/starboard/android/shared/configuration_public.h
index 371a413..2064399 100644
--- a/src/starboard/android/shared/configuration_public.h
+++ b/src/starboard/android/shared/configuration_public.h
@@ -23,12 +23,6 @@
 #ifndef STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
 #define STARBOARD_ANDROID_SHARED_CONFIGURATION_PUBLIC_H_
 
-#if SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-#error \
-    "This platform's sabi.json file is expected to track the experimental " \
-"Starboard API version."
-#endif  // SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-
 // --- Architecture Configuration --------------------------------------------
 
 // Indicates that there is no support for alignment at greater than 16 bytes for
diff --git a/src/starboard/android/shared/launcher.py b/src/starboard/android/shared/launcher.py
new file mode 100644
index 0000000..106b2b9
--- /dev/null
+++ b/src/starboard/android/shared/launcher.py
@@ -0,0 +1,437 @@
+#
+# 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 re
+import socket
+import subprocess
+import sys
+import threading
+import time
+import Queue
+
+import _env  # pylint: disable=unused-import,g-bad-import-order
+
+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'
+
+# How long to keep logging after a crash in order to emit the stack trace.
+_CRASH_LOG_SECONDS = 1.0
+
+_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, adb, device_id=None):
+    self.adb = adb
+    self.device_id = device_id
+
+  def Build(self, *args):
+    """Builds an 'adb' commandline with the given args."""
+    result = [self.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, launcher, done_queue):
+    self.launcher = launcher
+    self.process = launcher._PopenAdb(
+        'shell', 'am', 'monitor', stdout=subprocess.PIPE)
+    if abstract_launcher.ARG_DRYRUN in launcher.launcher_args:
+      self.thread = None
+      return
+    self.thread = threading.Thread(target=self._Run)
+    self.thread.start()
+    self.done_queue = done_queue
+
+  def Shutdown(self):
+    self.process.kill()
+    if self.thread:
+      self.thread.join()
+
+  def _Run(self):
+    while True:
+      line = CleanLine(self.process.stdout.readline())
+      if not line:
+        return
+      # Show the crash lines reported by "am monitor".
+      sys.stderr.write(line)
+      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
+        self.launcher.CallAdb('shell', 'log', '-t', 'starboard',
+                              'am monitor detected crash')
+
+
+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 abstract_launcher.ARG_SYSTOOLS in self.launcher_args:
+      # Use default adb binary from path.
+      self.adb = 'adb'
+    else:
+      self.adb = os.path.join(sdk_utils.GetSdkPath(), 'platform-tools', 'adb')
+
+    self.adb_builder = AdbCommandBuilder(self.adb)
+
+    if not self.device_id:
+      self.device_id = self._IdentifyDevice()
+    else:
+      self._ConnectIfNecessary()
+
+    self.adb_builder.device_id = self.device_id
+
+    # Verify connection and dump target build fingerprint.
+    self._CheckCallAdb('shell', 'getprop', 'ro.build.fingerprint')
+
+    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.
+      # pylint: disable=g-socket-inet-aton
+      if socket.inet_aton(address) and address.count('.') == 3:
+        return True
+    except Exception:  # pylint: disable=broad-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 = self._PopenAdb('devices', stdout=subprocess.PIPE)
+    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 = self._PopenAdb(
+        'connect',
+        '{}:5555'.format(self.device_id),
+        stderr=subprocess.STDOUT,
+        stdout=subprocess.PIPE,
+    )
+    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))
+      sys.stderr.write('connect command exited with code {} '
+                       'and returned: {}'.format(p.returncode, result))
+
+  def _Call(self, *args):
+    sys.stderr.write('{}\n'.format(' '.join(args)))
+    if abstract_launcher.ARG_DRYRUN not in self.launcher_args:
+      subprocess.call(args, 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)))
+    if abstract_launcher.ARG_DRYRUN not in self.launcher_args:
+      subprocess.check_call(args, close_fds=True)
+
+  def _CheckCallAdb(self, *in_args):
+    args = self.adb_builder.Build(*in_args)
+    self._CheckCall(*args)
+
+  def _PopenAdb(self, *args, **kwargs):
+    build_args = self.adb_builder.Build(*args)
+    sys.stderr.write('{}\n'.format(' '.join(build_args)))
+    if abstract_launcher.ARG_DRYRUN in self.launcher_args:
+      return subprocess.Popen(['echo', 'dry-run'])
+    return subprocess.Popen(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._CheckCallAdb('wait-for-device')
+    self._Shutdown()
+
+    # Clear logcat
+    self._CheckCallAdb('logcat', '-c')
+
+    # Install the APK, unless "noinstall" was specified.
+    if abstract_launcher.ARG_NOINSTALL not in self.launcher_args:
+      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', 'KEYCODE_WAKEUP')
+
+    # Grant runtime permissions to avoid prompts during testing.
+    if abstract_launcher.ARG_NOINSTALL not in self.launcher_args:
+      for permission in _RUNTIME_PERMISSIONS:
+        self._CheckCallAdb('shell', 'pm', 'grant', _APP_PACKAGE_NAME,
+                           permission)
+
+    done_queue = Queue.Queue()
+    am_monitor = AdbAmMonitorWatcher(self, 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',
+        '*:E',
+        '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',
+      ]
+      for param in self.target_command_line_params:
+        if param.startswith('--link='):
+          # Android deeplinks go in the Intent data
+          link = param.split('=')[1]
+          args += ['-d', "'{}'".format(link)]
+        else:
+          command_line_params.append(param)
+      args += ['--esa', 'args', "'{}'".format(','.join(command_line_params))]
+      args += [_APP_START_INTENT]
+
+      self._CheckCallAdb(*args)
+
+      run_loop = abstract_launcher.ARG_DRYRUN not in self.launcher_args
+
+      app_crashed = False
+      while run_loop:
+        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')
+        # Set return code to mimic segfault code on Linux
+        return_code = 11
+      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()
+      if logcat_process.poll() is None:
+        # This could happen when using SIGINT to kill the launcher
+        # (e.g. when using starboard/tools/example/app_launcher_client.py).
+        sys.stderr.write('Logcat process is still running. Killing it now.\n')
+        logcat_process.kill()
+
+    return return_code
+
+  def _Shutdown(self):
+    self.CallAdb('shell', 'am', 'force-stop', _APP_PACKAGE_NAME)
+
+  def SupportsDeepLink(self):
+    return True
+
+  def SendDeepLink(self, link):
+    shell_cmd = 'am start -d "{}" {}'.format(link, _APP_START_INTENT)
+    args = ['shell', shell_cmd]
+    self._CheckCallAdb(*args)
+    return True
+
+  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)
+    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))
+    # pylint: disable=g-socket-gethostbyname
+    return socket.gethostbyname('localhost'), self.local_port
+
+  def GetDeviceIp(self):
+    """Gets the device IP. TODO: Implement."""
+    return None
diff --git a/src/starboard/android/shared/player_components_factory.cc b/src/starboard/android/shared/player_components_factory.cc
index 6ac984a..01dc7a6 100644
--- a/src/starboard/android/shared/player_components_factory.cc
+++ b/src/starboard/android/shared/player_components_factory.cc
@@ -12,23 +12,7 @@
 // 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/video_decoder.h"
-#include "starboard/android/shared/video_render_algorithm.h"
-#include "starboard/common/log.h"
-#include "starboard/common/ref_counted.h"
-#include "starboard/common/scoped_ptr.h"
-#include "starboard/media.h"
-#include "starboard/shared/opus/opus_audio_decoder.h"
-#include "starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.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/player_components.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"
+#include "starboard/android/shared/player_components_factory.h"
 
 namespace starboard {
 namespace shared {
@@ -36,121 +20,10 @@
 namespace player {
 namespace filter {
 
-namespace {
-
-const int kAudioSinkFramesAlignment = 256;
-const int kDefaultAudioSinkMinFramesPerAppend = 1024;
-const int kDefaultAudioSinkMaxCachedFrames =
-    8 * kDefaultAudioSinkMinFramesPerAppend;
-
-int AlignUp(int value, int alignment) {
-  return (value + alignment - 1) / alignment * alignment;
-}
-
-class PlayerComponentsFactory : public PlayerComponents::Factory {
-  bool CreateSubComponents(
-      const CreationParameters& creation_parameters,
-      scoped_ptr<AudioDecoder>* audio_decoder,
-      scoped_ptr<AudioRendererSink>* audio_renderer_sink,
-      scoped_ptr<VideoDecoder>* video_decoder,
-      scoped_ptr<VideoRenderAlgorithm>* video_render_algorithm,
-      scoped_refptr<VideoRendererSink>* video_renderer_sink,
-      std::string* error_message) override {
-    SB_DCHECK(error_message);
-
-    if (creation_parameters.audio_codec() != kSbMediaAudioCodecNone) {
-      SB_DCHECK(audio_decoder);
-      SB_DCHECK(audio_renderer_sink);
-
-      auto decoder_creator = [](const SbMediaAudioSampleInfo& audio_sample_info,
-                                SbDrmSystem drm_system) {
-        if (audio_sample_info.codec == kSbMediaAudioCodecAac) {
-          scoped_ptr<android::shared::AudioDecoder> audio_decoder_impl(
-              new android::shared::AudioDecoder(audio_sample_info.codec,
-                                                audio_sample_info, drm_system));
-          if (audio_decoder_impl->is_valid()) {
-            return audio_decoder_impl.PassAs<AudioDecoder>();
-          }
-        } else if (audio_sample_info.codec == kSbMediaAudioCodecOpus) {
-          scoped_ptr<opus::OpusAudioDecoder> audio_decoder_impl(
-              new opus::OpusAudioDecoder(audio_sample_info));
-          if (audio_decoder_impl->is_valid()) {
-            return audio_decoder_impl.PassAs<AudioDecoder>();
-          }
-        } else {
-          SB_NOTREACHED();
-        }
-        return scoped_ptr<AudioDecoder>();
-      };
-
-      audio_decoder->reset(new AdaptiveAudioDecoder(
-          creation_parameters.audio_sample_info(),
-          creation_parameters.drm_system(), decoder_creator));
-      audio_renderer_sink->reset(new AudioRendererSinkImpl);
-    }
-
-    if (creation_parameters.video_codec() != kSbMediaVideoCodecNone) {
-      SB_DCHECK(video_decoder);
-      SB_DCHECK(video_render_algorithm);
-      SB_DCHECK(video_renderer_sink);
-      SB_DCHECK(error_message);
-
-      scoped_ptr<android::shared::VideoDecoder> video_decoder_impl(
-          new android::shared::VideoDecoder(
-              creation_parameters.video_codec(),
-              creation_parameters.drm_system(),
-              creation_parameters.output_mode(),
-              creation_parameters.decode_target_graphics_context_provider(),
-              creation_parameters.max_video_capabilities(), error_message));
-      if (video_decoder_impl->is_valid()) {
-        video_render_algorithm->reset(new android::shared::VideoRenderAlgorithm(
-            video_decoder_impl.get()));
-        *video_renderer_sink = video_decoder_impl->GetSink();
-        video_decoder->reset(video_decoder_impl.release());
-      } else {
-        video_decoder->reset();
-        *video_renderer_sink = NULL;
-        *error_message =
-            "Failed to create video decoder with error: " + *error_message;
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  void GetAudioRendererParams(const CreationParameters& creation_parameters,
-                              int* max_cached_frames,
-                              int* min_frames_per_append) const override {
-    SB_DCHECK(max_cached_frames);
-    SB_DCHECK(min_frames_per_append);
-    SB_DCHECK(kDefaultAudioSinkMinFramesPerAppend % kAudioSinkFramesAlignment ==
-              0);
-    *min_frames_per_append = kDefaultAudioSinkMinFramesPerAppend;
-
-    // AudioRenderer prefers to use kSbMediaAudioSampleTypeFloat32 and only uses
-    // kSbMediaAudioSampleTypeInt16Deprecated when float32 is not supported.
-    int min_frames_required = SbAudioSinkGetMinBufferSizeInFrames(
-        creation_parameters.audio_sample_info().number_of_channels,
-        SbAudioSinkIsAudioSampleTypeSupported(kSbMediaAudioSampleTypeFloat32)
-            ? kSbMediaAudioSampleTypeFloat32
-            : kSbMediaAudioSampleTypeInt16Deprecated,
-        creation_parameters.audio_sample_info().samples_per_second);
-    // On Android 5.0, the size of audio renderer sink buffer need to be two
-    // times larger than AudioTrack minBufferSize. Otherwise, AudioTrack may
-    // stop working after pause.
-    *max_cached_frames =
-        min_frames_required * 2 + kDefaultAudioSinkMinFramesPerAppend;
-    *max_cached_frames = AlignUp(*max_cached_frames, kAudioSinkFramesAlignment);
-  }
-};
-
-}  // namespace
-
 // static
 scoped_ptr<PlayerComponents::Factory> PlayerComponents::Factory::Create() {
   return make_scoped_ptr<PlayerComponents::Factory>(
-      new PlayerComponentsFactory);
+      new android::shared::PlayerComponentsFactory);
 }
 
 // static
diff --git a/src/starboard/android/shared/player_components_factory.h b/src/starboard/android/shared/player_components_factory.h
new file mode 100644
index 0000000..350b00f
--- /dev/null
+++ b/src/starboard/android/shared/player_components_factory.h
@@ -0,0 +1,173 @@
+// 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_PLAYER_COMPONENTS_FACTORY_H_
+#define STARBOARD_ANDROID_SHARED_PLAYER_COMPONENTS_FACTORY_H_
+
+#include <string>
+
+#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/log.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/common/scoped_ptr.h"
+#include "starboard/media.h"
+#include "starboard/shared/opus/opus_audio_decoder.h"
+#include "starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.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/player_components.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 android {
+namespace shared {
+
+class PlayerComponentsFactory : public starboard::shared::starboard::player::
+                                    filter::PlayerComponents::Factory {
+  typedef starboard::shared::opus::OpusAudioDecoder OpusAudioDecoder;
+  typedef starboard::shared::starboard::player::filter::AdaptiveAudioDecoder
+      AdaptiveAudioDecoder;
+  typedef starboard::shared::starboard::player::filter::AudioDecoder
+      AudioDecoderBase;
+  typedef starboard::shared::starboard::player::filter::AudioRendererSink
+      AudioRendererSink;
+  typedef starboard::shared::starboard::player::filter::AudioRendererSinkImpl
+      AudioRendererSinkImpl;
+  typedef starboard::shared::starboard::player::filter::VideoDecoder
+      VideoDecoderBase;
+  typedef starboard::shared::starboard::player::filter::VideoRenderAlgorithm
+      VideoRenderAlgorithmBase;
+  typedef starboard::shared::starboard::player::filter::VideoRendererSink
+      VideoRendererSink;
+
+  const int kAudioSinkFramesAlignment = 256;
+  const int kDefaultAudioSinkMinFramesPerAppend = 1024;
+  const int kDefaultAudioSinkMaxCachedFrames =
+      8 * kDefaultAudioSinkMinFramesPerAppend;
+
+  virtual SbDrmSystem GetExtendedDrmSystem(SbDrmSystem drm_system) {
+    return drm_system;
+  }
+
+  static int AlignUp(int value, int alignment) {
+    return (value + alignment - 1) / alignment * alignment;
+  }
+
+  bool CreateSubComponents(
+      const CreationParameters& creation_parameters,
+      scoped_ptr<AudioDecoderBase>* audio_decoder,
+      scoped_ptr<AudioRendererSink>* audio_renderer_sink,
+      scoped_ptr<VideoDecoderBase>* video_decoder,
+      scoped_ptr<VideoRenderAlgorithmBase>* video_render_algorithm,
+      scoped_refptr<VideoRendererSink>* video_renderer_sink,
+      std::string* error_message) override {
+    SB_DCHECK(error_message);
+
+    if (creation_parameters.audio_codec() != kSbMediaAudioCodecNone) {
+      SB_DCHECK(audio_decoder);
+      SB_DCHECK(audio_renderer_sink);
+
+      auto decoder_creator = [](const SbMediaAudioSampleInfo& audio_sample_info,
+                                SbDrmSystem drm_system) {
+        if (audio_sample_info.codec == kSbMediaAudioCodecAac) {
+          scoped_ptr<AudioDecoder> audio_decoder_impl(new AudioDecoder(
+              audio_sample_info.codec, audio_sample_info, drm_system));
+          if (audio_decoder_impl->is_valid()) {
+            return audio_decoder_impl.PassAs<AudioDecoderBase>();
+          }
+        } else if (audio_sample_info.codec == kSbMediaAudioCodecOpus) {
+          scoped_ptr<OpusAudioDecoder> audio_decoder_impl(
+              new OpusAudioDecoder(audio_sample_info));
+          if (audio_decoder_impl->is_valid()) {
+            return audio_decoder_impl.PassAs<AudioDecoderBase>();
+          }
+        } else {
+          SB_NOTREACHED();
+        }
+        return scoped_ptr<AudioDecoderBase>();
+      };
+
+      audio_decoder->reset(new AdaptiveAudioDecoder(
+          creation_parameters.audio_sample_info(),
+          GetExtendedDrmSystem(creation_parameters.drm_system()),
+          decoder_creator));
+      audio_renderer_sink->reset(new AudioRendererSinkImpl);
+    }
+
+    if (creation_parameters.video_codec() != kSbMediaVideoCodecNone) {
+      SB_DCHECK(video_decoder);
+      SB_DCHECK(video_render_algorithm);
+      SB_DCHECK(video_renderer_sink);
+      SB_DCHECK(error_message);
+
+      scoped_ptr<VideoDecoder> video_decoder_impl(new VideoDecoder(
+          creation_parameters.video_codec(),
+          GetExtendedDrmSystem(creation_parameters.drm_system()),
+          creation_parameters.output_mode(),
+          creation_parameters.decode_target_graphics_context_provider(),
+          creation_parameters.max_video_capabilities(), error_message));
+      if (video_decoder_impl->is_valid()) {
+        video_render_algorithm->reset(
+            new VideoRenderAlgorithm(video_decoder_impl.get()));
+        *video_renderer_sink = video_decoder_impl->GetSink();
+        video_decoder->reset(video_decoder_impl.release());
+      } else {
+        video_decoder->reset();
+        *video_renderer_sink = NULL;
+        *error_message =
+            "Failed to create video decoder with error: " + *error_message;
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  void GetAudioRendererParams(const CreationParameters& creation_parameters,
+                              int* max_cached_frames,
+                              int* min_frames_per_append) const override {
+    SB_DCHECK(max_cached_frames);
+    SB_DCHECK(min_frames_per_append);
+    SB_DCHECK(kDefaultAudioSinkMinFramesPerAppend % kAudioSinkFramesAlignment ==
+              0);
+    *min_frames_per_append = kDefaultAudioSinkMinFramesPerAppend;
+
+    // AudioRenderer prefers to use kSbMediaAudioSampleTypeFloat32 and only uses
+    // kSbMediaAudioSampleTypeInt16Deprecated when float32 is not supported.
+    int min_frames_required = SbAudioSinkGetMinBufferSizeInFrames(
+        creation_parameters.audio_sample_info().number_of_channels,
+        SbAudioSinkIsAudioSampleTypeSupported(kSbMediaAudioSampleTypeFloat32)
+            ? kSbMediaAudioSampleTypeFloat32
+            : kSbMediaAudioSampleTypeInt16Deprecated,
+        creation_parameters.audio_sample_info().samples_per_second);
+    // On Android 5.0, the size of audio renderer sink buffer need to be two
+    // times larger than AudioTrack minBufferSize. Otherwise, AudioTrack may
+    // stop working after pause.
+    *max_cached_frames =
+        min_frames_required * 2 + kDefaultAudioSinkMinFramesPerAppend;
+    *max_cached_frames = AlignUp(*max_cached_frames, kAudioSinkFramesAlignment);
+  }
+};
+
+}  // namespace shared
+}  // namespace android
+}  // namespace starboard
+
+#endif  // STARBOARD_ANDROID_SHARED_PLAYER_COMPONENTS_FACTORY_H_
diff --git a/src/starboard/android/shared/starboard_platform.gypi b/src/starboard/android/shared/starboard_platform.gypi
index 8ec8f70..8b27a29 100644
--- a/src/starboard/android/shared/starboard_platform.gypi
+++ b/src/starboard/android/shared/starboard_platform.gypi
@@ -134,6 +134,7 @@
         'media_is_audio_supported.cc',
         'media_is_video_supported.cc',
         'microphone_impl.cc',
+        'player_components_factory.h',
         'player_create.cc',
         'player_destroy.cc',
         'player_get_preferred_output_mode.cc',
diff --git a/src/starboard/android/x86/__init__.py b/src/starboard/android/x86/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/x86/__init__.py
diff --git a/src/starboard/android/x86/cobalt/__init__.py b/src/starboard/android/x86/cobalt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/starboard/android/x86/cobalt/__init__.py
diff --git a/src/starboard/android/x86/cobalt/configuration.py b/src/starboard/android/x86/cobalt/configuration.py
new file mode 100644
index 0000000..5e11fd8
--- /dev/null
+++ b/src/starboard/android/x86/cobalt/configuration.py
@@ -0,0 +1,61 @@
+# Copyright 2017-2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Starboard Android x86 Cobalt configuration."""
+
+from starboard.android.shared.cobalt import configuration
+from starboard.tools.testing import test_filter
+
+
+class CobaltAndroidX86Configuration(configuration.CobaltAndroidConfiguration):
+  """Starboard Android x86 Cobalt configuration."""
+
+  def GetTestFilters(self):
+    filters = super(CobaltAndroidX86Configuration, 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 = {  # pylint: disable=invalid-name
+      'graphics_system_test': [
+          test_filter.FILTER_ALL
+      ],
+      'layout_tests': [  # Old Android versions don't have matching fonts
+          'CSS3FontsLayoutTests/Layout.Test'
+          '/5_2_use_first_available_listed_font_family',
+          'CSS3FontsLayoutTests/Layout.Test'
+          '/5_2_use_specified_font_family_if_available',
+          'CSS3FontsLayoutTests/Layout.Test'
+          '/5_2_use_system_fallback_if_no_matching_family_is_found*',
+          'CSS3FontsLayoutTests/Layout.Test'
+          '/synthetic_bolding_should_not_occur_on_bold_font',
+          'CSS3FontsLayoutTests/Layout.Test'
+          '/synthetic_bolding_should_occur_on_non_bold_font',
+      ],
+      'nb_test': [
+          'BidirectionalFitReuseAllocatorTest.FallbackBlockMerge',
+          'BidirectionalFitReuseAllocatorTest.FreeBlockMergingLeft',
+          'BidirectionalFitReuseAllocatorTest.FreeBlockMergingRight',
+          'FirstFitReuseAllocatorTest.FallbackBlockMerge',
+          'FirstFitReuseAllocatorTest.FreeBlockMergingLeft',
+          'FirstFitReuseAllocatorTest.FreeBlockMergingRight',
+      ],
+      'net_unittests': [  # Net tests are very unstable on Android L
+          test_filter.FILTER_ALL
+      ],
+      'renderer_test': [
+          'PixelTest.YUV422UYVYImageScaledUpSupport',
+          'PixelTest.YUV422UYVYImageScaledAndTranslated',
+      ],
+  }
diff --git a/src/starboard/android/x86/gyp_configuration.py b/src/starboard/android/x86/gyp_configuration.py
index 80134e6..e3e4c2c 100644
--- a/src/starboard/android/x86/gyp_configuration.py
+++ b/src/starboard/android/x86/gyp_configuration.py
@@ -1,4 +1,4 @@
-# Copyright 2016 The Cobalt Authors. All Rights Reserved.
+# Copyright 2016-2020 The Cobalt Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,10 +14,41 @@
 """Starboard Android x86 platform build configuration."""
 
 from starboard.android.shared import gyp_configuration as shared_configuration
+from starboard.tools.testing import test_filter
 
 
 def CreatePlatformConfig():
-  return shared_configuration.AndroidConfiguration(
+  return Androidx86Configuration(
       'android-x86',
       'x86',
       sabi_json_path='starboard/sabi/x86/sabi-v{sb_api_version}.json')
+
+
+class Androidx86Configuration(shared_configuration.AndroidConfiguration):
+
+  def GetTestFilters(self):
+    filters = super(Androidx86Configuration, 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 = {  # pylint: disable=invalid-name
+      'nplb': [
+          'SbAccessibilityTest.CallSetCaptionsEnabled',
+          'SbAccessibilityTest.GetCaptionSettingsReturnIsValid',
+          'SbAudioSinkTest.*',
+          'SbMicrophoneCloseTest.*',
+          'SbMicrophoneOpenTest.*',
+          'SbMicrophoneReadTest.*',
+          'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.*',
+          'SbSocketAddressTypes/SbSocketGetInterfaceAddressTest'
+          '.SunnyDaySourceForDestination/*',
+          'SbMediaSetAudioWriteDurationTests/SbMediaSetAudioWriteDurationTest'
+          '.WriteContinuedLimitedInput/*',
+      ],
+      'player_filter_tests': [
+          'VideoDecoderTests/VideoDecoderTest.MaxNumberOfCachedFrames/2',
+          'AudioDecoderTests/*',
+      ],
+  }
diff --git a/src/starboard/build/base_configuration.gypi b/src/starboard/build/base_configuration.gypi
index b8f6d94..a723315 100644
--- a/src/starboard/build/base_configuration.gypi
+++ b/src/starboard/build/base_configuration.gypi
@@ -81,6 +81,9 @@
     # Whether this is an evergreen build.
     'sb_evergreen': 0,
 
+    # Whether to use crashpad.
+    'sb_crashpad_enabled': 0,
+
     # Whether this is an evergreen compatible platform. A compatible platform
     # can run the elf_loader and launch the evergreen build.
     'sb_evergreen_compatible%': '<(sb_evergreen_compatible)',
diff --git a/src/starboard/build/gyp_functions.py b/src/starboard/build/gyp_functions.py
new file mode 100644
index 0000000..e6f466d
--- /dev/null
+++ b/src/starboard/build/gyp_functions.py
@@ -0,0 +1,177 @@
+# Copyright 2020 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Gyp extensions, called with pymod_do_main."""
+import argparse
+import glob
+import logging
+import os
+import stat
+import sys
+
+
+# lifted from standard lib webbrowser.py
+def isexecutable(cmd):
+  """Returns whether the input file is an exectuable."""
+  if sys.platform[:3] == 'win':
+    extensions = ('.exe', '.bat', '.cmd')
+    cmd = cmd.lower()
+    if cmd.endswith(extensions) and os.path.isfile(cmd):
+      return cmd
+    for ext in extensions:
+      if os.path.isfile(cmd + ext):
+        return cmd + ext
+  else:
+    if os.path.isfile(cmd):
+      mode = os.stat(cmd)[stat.ST_MODE]
+      if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
+        return cmd
+
+
+class ExtensionCommandParser(argparse.ArgumentParser):
+  """Helper class for parsing arguments to extended gyp functions."""
+
+  def __init__(self, list_args):
+    argparse.ArgumentParser.__init__(self)
+    for arg in list_args:
+      self.add_argument(arg)
+
+  def error(self, message):
+    print('Parse error in gyp_functions.py:' + message)
+    raise NotImplementedError('Parse error:' + message)
+
+
+class Extensions(object):
+  """Container class for extended functionality for gyp.
+
+  Supports operations such as:
+    - file globbing
+    - checking file or directory existence
+    - string manipulations
+    - retrieving environment variables
+    - searching PATH and PYTHONPATH for programs.
+  """
+
+  def __init__(self, argv):
+    parser = argparse.ArgumentParser()
+    all_cmds = [x for x in dir(self) if not x.startswith('_')]
+    parser.add_argument('command', help='Command to run', choices=all_cmds)
+    args = parser.parse_args(argv[0:1])
+    self.argv = argv[1:]
+    self.command = args.command
+
+  def call(self):
+    return getattr(self, self.command)()
+
+  def file_glob(self):
+    """Glob files in dir, with pattern glob."""
+    args = ExtensionCommandParser(['dir', 'pattern']).parse_args(self.argv)
+    path = os.path.normpath(os.path.join(args.dir, args.pattern))
+    ret = ''
+    for f in glob.iglob(path):
+      ret += f.replace(os.sep, '/') + ' '
+    return ret.strip()
+
+  def basename(self):
+    """Basename of list of files"""
+    parser = ExtensionCommandParser([])
+    parser.add_argument('input_list', nargs='*')
+    args = parser.parse_args(self.argv)
+    ret = [os.path.basename(x) for x in args.input_list]
+    return ' '.join(ret)
+
+  def replace_in_list(self):
+    """String replace in a list of arguments"""
+    parser = ExtensionCommandParser(['old', 'new'])
+    parser.add_argument('input_list', nargs='*')
+    args = parser.parse_args(self.argv)
+    inp = args.input_list
+    return ' '.join([x.replace(args.old, args.new) for x in inp])
+
+  def file_glob_sub(self):
+    """Glob files, but return filenames with string replace from->to applied."""
+    args = ExtensionCommandParser(
+        ['dir', 'pattern', 'from_string', 'to_string']).parse_args(self.argv)
+    path = os.path.normpath(os.path.join(args.dir, args.pattern))
+    ret = ''
+    for f in glob.iglob(path):
+      ret += f.replace(os.sep, '/').replace(args.from_string,
+                                            args.to_string) + ' '
+    return ret.strip()
+
+  def file_exists(self):
+    """Checks if a file exists, returning a string '1' if so, or '0'."""
+    args = ExtensionCommandParser(['file']).parse_args(self.argv)
+    filepath = args.file.replace(os.sep, '/')
+    ret = os.path.isfile(filepath)
+    return str(int(ret))
+
+  def dir_exists(self):
+    """Checks if a directory exists, returning a string 'True' or 'False'."""
+    args = ExtensionCommandParser(['dir']).parse_args(self.argv)
+    return str(os.path.isdir(args.dir))
+
+  def str_upper(self):
+    """Converts an input string to upper case."""
+    if self.argv:
+      args = ExtensionCommandParser(['str']).parse_args(self.argv)
+      return args.str.upper()
+    return ''
+
+  def find_program(self):
+    """Searches for the input program name (.exe, .cmd or .bat)."""
+    args = ExtensionCommandParser(['program']).parse_args(self.argv)
+    paths_to_check = []
+    # Collect all paths in PYTHONPATH.
+    for i in sys.path:
+      paths_to_check.append(i.replace(os.sep, '/'))
+    # Then collect all paths in PATH.
+    for i in os.environ['PATH'].split(os.pathsep):
+      paths_to_check.append(i.replace(os.sep, '/'))
+    # Check all the collected paths for the program.
+    for path in paths_to_check:
+      exe = os.path.join(path, args.program)
+      prog = isexecutable(exe)
+      if prog:
+        return prog.replace(os.sep, '/')
+    # If not in PYTHONPATH and PATH, check upwards until root.
+    # Note: This is a rare case.
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    previous_dir = os.path.abspath(__file__)
+    while root_dir and root_dir != previous_dir:
+      exe = os.path.join(root_dir, args.program)
+      prog = isexecutable(exe)
+      if prog:
+        return prog.replace(os.sep, '/')
+      previous_dir = root_dir
+      root_dir = os.path.dirname(root_dir)
+    logging.error('Failed to find program "{}".'.format(args.program))
+    return None
+
+  def getenv(self):
+    """Gets the stored value of an environment variable."""
+    args = ExtensionCommandParser(['var']).parse_args(self.argv)
+    value = os.getenv(args.var)
+    if value is not None:
+      return value.strip()
+    return ''
+
+
+def DoMain(argv):  # pylint: disable=invalid-name
+  """Script main function."""
+  return Extensions(argv).call()
+
+
+if __name__ == '__main__':
+  print(DoMain(sys.argv[1:]))
+  sys.exit(0)
diff --git a/src/starboard/common/configuration_defaults.cc b/src/starboard/common/configuration_defaults.cc
index 6dbeb7b..6a933b9 100644
--- a/src/starboard/common/configuration_defaults.cc
+++ b/src/starboard/common/configuration_defaults.cc
@@ -30,7 +30,11 @@
 }
 
 const char* CobaltFallbackSplashScreenUrlDefault() {
-  return "none";
+  return "h5vcc-embedded://black_splash_screen.html";
+}
+
+const char* CobaltFallbackSplashScreenTopicsDefault() {
+  return "";
 }
 
 bool CobaltEnableQuicDefault() {
diff --git a/src/starboard/common/configuration_defaults.h b/src/starboard/common/configuration_defaults.h
index c79c210..06114f9 100644
--- a/src/starboard/common/configuration_defaults.h
+++ b/src/starboard/common/configuration_defaults.h
@@ -26,6 +26,8 @@
 
 const char* CobaltFallbackSplashScreenUrlDefault();
 
+const char* CobaltFallbackSplashScreenTopicsDefault();
+
 bool CobaltEnableQuicDefault();
 
 int CobaltSkiaCacheSizeInBytesDefault();
diff --git a/src/starboard/elf_loader/elf_loader.gyp b/src/starboard/elf_loader/elf_loader.gyp
index 7b6a565..4970ae0 100644
--- a/src/starboard/elf_loader/elf_loader.gyp
+++ b/src/starboard/elf_loader/elf_loader.gyp
@@ -53,21 +53,17 @@
         'src/include',
         'src/src/',
       ],
+      'conditions': [
+        ['sb_evergreen_compatible == 1', {
+          'variables': {
+            'sb_crashpad_enabled': 1,
+          },
+        },],
+      ],
       'dependencies': [
         '<(DEPTH)/starboard/elf_loader/evergreen_config.gyp:evergreen_config',
         '<(DEPTH)/starboard/elf_loader/evergreen_info.gyp:evergreen_info',
-        '<(DEPTH)/starboard/starboard.gyp:starboard_base',
-      ],
-      'conditions': [
-        ['sb_evergreen_compatible == 1', {
-          'dependencies': [
-            '<(DEPTH)/third_party/crashpad/wrapper/wrapper.gyp:crashpad_wrapper',
-          ],
-        }, {
-          'dependencies': [
-            '<(DEPTH)/third_party/crashpad/wrapper/wrapper.gyp:crashpad_wrapper_stub',
-          ],
-        }],
+        '<(DEPTH)/starboard/starboard.gyp:starboard',
       ],
       'sources': [
         '<@(common_elf_loader_sources)',
@@ -113,7 +109,7 @@
       'dependencies': [
         'elf_loader',
         '<(DEPTH)/cobalt/content/fonts/fonts.gyp:copy_font_data',
-        '<(DEPTH)/starboard/starboard.gyp:starboard_full',
+        '<(DEPTH)/starboard/starboard.gyp:starboard',
         # TODO: Remove this dependency once MediaSession is migrated to use CobaltExtensions.
         '<@(cobalt_platform_dependencies)',
       ],
@@ -149,7 +145,7 @@
       ],
       'dependencies': [
         'elf_loader_sys',
-        '<(DEPTH)/starboard/starboard.gyp:starboard_full',
+        '<(DEPTH)/starboard/starboard.gyp:starboard',
       ],
       'sources': [
         'sandbox.cc',
@@ -166,7 +162,7 @@
         '<(DEPTH)/starboard/common/test_main.cc',
       ],
       'dependencies': [
-        '<(DEPTH)/starboard/starboard.gyp:starboard_full',
+        '<(DEPTH)/starboard/starboard.gyp:starboard',
         # TODO: Remove this dependency once MediaSession is migrated to use CobaltExtensions.
         '<@(cobalt_platform_dependencies)',
         '<(DEPTH)/testing/gmock.gyp:gmock',
diff --git a/src/starboard/linux/shared/configuration.cc b/src/starboard/linux/shared/configuration.cc
index 4b0f775..bb7371e 100644
--- a/src/starboard/linux/shared/configuration.cc
+++ b/src/starboard/linux/shared/configuration.cc
@@ -32,7 +32,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &CobaltEglSwapInterval,
@@ -55,6 +55,7 @@
     &common::CobaltGcZealDefault,
     &common::CobaltRasterizerTypeDefault,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/linux/x64x11/configuration_public.h b/src/starboard/linux/x64x11/configuration_public.h
index 38bcc2e..1fa3098 100644
--- a/src/starboard/linux/x64x11/configuration_public.h
+++ b/src/starboard/linux/x64x11/configuration_public.h
@@ -22,12 +22,6 @@
 #ifndef STARBOARD_LINUX_X64X11_CONFIGURATION_PUBLIC_H_
 #define STARBOARD_LINUX_X64X11_CONFIGURATION_PUBLIC_H_
 
-#if SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-#error \
-    "This platform's sabi.json file is expected to track the experimental " \
-"Starboard API version."
-#endif  // SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-
 // --- Architecture Configuration --------------------------------------------
 
 // Configuration parameters that allow the application to make some general
diff --git a/src/starboard/linux/x64x11/gczeal/configuration.cc b/src/starboard/linux/x64x11/gczeal/configuration.cc
index 6d80740..5a88df1 100644
--- a/src/starboard/linux/x64x11/gczeal/configuration.cc
+++ b/src/starboard/linux/x64x11/gczeal/configuration.cc
@@ -38,7 +38,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &CobaltEglSwapInterval,
@@ -61,6 +61,7 @@
     &CobaltGcZeal,
     &common::CobaltRasterizerTypeDefault,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/linux/x64x11/skia/configuration.cc b/src/starboard/linux/x64x11/skia/configuration.cc
index 3b2e26a..0c57f18 100644
--- a/src/starboard/linux/x64x11/skia/configuration.cc
+++ b/src/starboard/linux/x64x11/skia/configuration.cc
@@ -38,7 +38,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &CobaltEglSwapInterval,
@@ -61,6 +61,7 @@
     &common::CobaltGcZealDefault,
     &CobaltRasterizerType,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/linux/x64x11/skia/configuration_public.h b/src/starboard/linux/x64x11/skia/configuration_public.h
index 8879563..ed69d67a 100644
--- a/src/starboard/linux/x64x11/skia/configuration_public.h
+++ b/src/starboard/linux/x64x11/skia/configuration_public.h
@@ -18,14 +18,6 @@
 #ifndef STARBOARD_LINUX_X64X11_SKIA_CONFIGURATION_PUBLIC_H_
 #define STARBOARD_LINUX_X64X11_SKIA_CONFIGURATION_PUBLIC_H_
 
-// This is not a released configuration, so it should implement the
-// experimental API version to validate trunk's viability.
-#if SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-#error \
-    "This platform's sabi.json file is expected to track the experimental " \
-"Starboard API version."
-#endif  // SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-
 // Include the X64X11 Linux configuration.
 #include "starboard/linux/x64x11/configuration_public.h"
 
diff --git a/src/starboard/loader_app/installation_manager.cc b/src/starboard/loader_app/installation_manager.cc
index 9325c94..0401b50 100644
--- a/src/starboard/loader_app/installation_manager.cc
+++ b/src/starboard/loader_app/installation_manager.cc
@@ -574,20 +574,23 @@
 
 bool InstallationManager::SaveInstallationStore() {
   ValidatePriorities();
-  char buf[IM_MAX_INSTALLATION_STORE_SIZE];
+
   if (IM_MAX_INSTALLATION_STORE_SIZE < installation_store_.ByteSize()) {
     SB_LOG(ERROR) << "SaveInstallationStore: Data too large"
                   << installation_store_.ByteSize();
     return false;
   }
 
+  const size_t buf_size = installation_store_.ByteSize();
+  std::vector<char> buf(buf_size, 0);
   loader_app::SetPendingRestart(
       installation_store_.roll_forward_to_installation() != -1);
 
-  installation_store_.SerializeToArray(buf, installation_store_.ByteSize());
+  installation_store_.SerializeToArray(buf.data(),
+                                       installation_store_.ByteSize());
 
 #if SB_API_VERSION >= 12
-  if (!SbFileAtomicReplace(store_path_.c_str(), buf,
+  if (!SbFileAtomicReplace(store_path_.c_str(), buf.data(),
                            installation_store_.ByteSize())) {
     SB_LOG(ERROR)
         << "SaveInstallationStore: Failed to store installation store: "
diff --git a/src/starboard/nplb/mutex_acquire_test.cc b/src/starboard/nplb/mutex_acquire_test.cc
index c37a465..81ad198 100644
--- a/src/starboard/nplb/mutex_acquire_test.cc
+++ b/src/starboard/nplb/mutex_acquire_test.cc
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
 #include "starboard/configuration.h"
+#include "starboard/mutex.h"
 #include "starboard/thread.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
diff --git a/src/starboard/nplb/mutex_acquire_try_test.cc b/src/starboard/nplb/mutex_acquire_try_test.cc
index ccd7e5b..e38e897 100644
--- a/src/starboard/nplb/mutex_acquire_try_test.cc
+++ b/src/starboard/nplb/mutex_acquire_try_test.cc
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
 #include "starboard/configuration.h"
+#include "starboard/mutex.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if SB_API_VERSION >= 12
diff --git a/src/starboard/nplb/mutex_create_test.cc b/src/starboard/nplb/mutex_create_test.cc
index 8c11eaa..f52ba08 100644
--- a/src/starboard/nplb/mutex_create_test.cc
+++ b/src/starboard/nplb/mutex_create_test.cc
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
 #include "starboard/configuration.h"
+#include "starboard/mutex.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if SB_API_VERSION >= 12
diff --git a/src/starboard/nplb/mutex_destroy_test.cc b/src/starboard/nplb/mutex_destroy_test.cc
index e555b70..5df8a3a 100644
--- a/src/starboard/nplb/mutex_destroy_test.cc
+++ b/src/starboard/nplb/mutex_destroy_test.cc
@@ -14,8 +14,8 @@
 
 // Destroy is mostly Sunny Day tested in Create.
 
-#include "starboard/common/mutex.h"
 #include "starboard/configuration.h"
+#include "starboard/mutex.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace starboard {
diff --git a/src/starboard/nplb/nplb_evergreen_compat_tests/sabi_test.cc b/src/starboard/nplb/nplb_evergreen_compat_tests/sabi_test.cc
index 60b6d6e..ac659ac 100644
--- a/src/starboard/nplb/nplb_evergreen_compat_tests/sabi_test.cc
+++ b/src/starboard/nplb/nplb_evergreen_compat_tests/sabi_test.cc
@@ -155,6 +155,12 @@
 };
 
 TEST_F(SabiTest, VerifySABI) {
+  SB_LOG(INFO) << "Using SB_API_VERSION=" << SB_API_VERSION;
+  SB_LOG(INFO) << "Using SABI=" << SB_SABI_JSON_ID;
+
+  ASSERT_LT(SB_API_VERSION, SB_EXPERIMENTAL_API_VERSION)
+      << "Evergreen should use SB_API_VERSION < SB_EXPERIMENTAL_API_VERSION";
+
   std::set<std::string> sabi_set;
   sabi_set.insert(kSabiJsonIdArmHardfp);
   sabi_set.insert(kSabiJsonIdArmSoftfp);
diff --git a/src/starboard/raspi/2/skia/configuration.cc b/src/starboard/raspi/2/skia/configuration.cc
index 58a6f11..925b92b 100644
--- a/src/starboard/raspi/2/skia/configuration.cc
+++ b/src/starboard/raspi/2/skia/configuration.cc
@@ -38,7 +38,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &common::CobaltEglSwapIntervalDefault,
@@ -61,6 +61,7 @@
     &common::CobaltGcZealDefault,
     &CobaltRasterizerType,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/raspi/shared/configuration.cc b/src/starboard/raspi/shared/configuration.cc
index b3a9f0b..74ab33e 100644
--- a/src/starboard/raspi/shared/configuration.cc
+++ b/src/starboard/raspi/shared/configuration.cc
@@ -32,7 +32,7 @@
 }
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &common::CobaltEglSwapIntervalDefault,
@@ -55,6 +55,7 @@
     &common::CobaltGcZealDefault,
     &common::CobaltRasterizerTypeDefault,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/raspi/shared/configuration_public.h b/src/starboard/raspi/shared/configuration_public.h
index 05988f6..3478725 100644
--- a/src/starboard/raspi/shared/configuration_public.h
+++ b/src/starboard/raspi/shared/configuration_public.h
@@ -17,12 +17,6 @@
 #ifndef STARBOARD_RASPI_SHARED_CONFIGURATION_PUBLIC_H_
 #define STARBOARD_RASPI_SHARED_CONFIGURATION_PUBLIC_H_
 
-#if SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-#error \
-    "This platform's sabi.json file is expected to track the experimental " \
-"Starboard API version."
-#endif  // SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-
 // --- Architecture Configuration --------------------------------------------
 
 // --- System Header Configuration -------------------------------------------
diff --git a/src/starboard/sabi/sabi.py b/src/starboard/sabi/sabi.py
index 09d04a3..3d95166 100644
--- a/src/starboard/sabi/sabi.py
+++ b/src/starboard/sabi/sabi.py
@@ -13,4 +13,4 @@
 # limitations under the License.
 """Source of truth of the default/experimental Starboard API version."""
 
-SB_API_VERSION = 13
+SB_API_VERSION = 12
diff --git a/src/starboard/shared/pthread/mutex_acquire.cc b/src/starboard/shared/pthread/mutex_acquire.cc
index 67f26b4..6183f7d 100644
--- a/src/starboard/shared/pthread/mutex_acquire.cc
+++ b/src/starboard/shared/pthread/mutex_acquire.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 #include <pthread.h>
 
diff --git a/src/starboard/shared/pthread/mutex_acquire_try.cc b/src/starboard/shared/pthread/mutex_acquire_try.cc
index 4c216a5..8c5c241 100644
--- a/src/starboard/shared/pthread/mutex_acquire_try.cc
+++ b/src/starboard/shared/pthread/mutex_acquire_try.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 #include <pthread.h>
 
diff --git a/src/starboard/shared/pthread/mutex_create.cc b/src/starboard/shared/pthread/mutex_create.cc
index e72884d..55b6a94 100644
--- a/src/starboard/shared/pthread/mutex_create.cc
+++ b/src/starboard/shared/pthread/mutex_create.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 #include <pthread.h>
 
diff --git a/src/starboard/shared/pthread/mutex_destroy.cc b/src/starboard/shared/pthread/mutex_destroy.cc
index c131595..f2d0b15 100644
--- a/src/starboard/shared/pthread/mutex_destroy.cc
+++ b/src/starboard/shared/pthread/mutex_destroy.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 #include <pthread.h>
 
diff --git a/src/starboard/shared/pthread/mutex_release.cc b/src/starboard/shared/pthread/mutex_release.cc
index fcd7de7..d2ce183 100644
--- a/src/starboard/shared/pthread/mutex_release.cc
+++ b/src/starboard/shared/pthread/mutex_release.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 #include <pthread.h>
 
diff --git a/src/starboard/shared/signal/suspend_signals.cc b/src/starboard/shared/signal/suspend_signals.cc
index e58e065..c728e5f 100644
--- a/src/starboard/shared/signal/suspend_signals.cc
+++ b/src/starboard/shared/signal/suspend_signals.cc
@@ -58,26 +58,10 @@
   ::sigaction(signal_id, &action, NULL);
 }
 
-#if SB_IS(EVERGREEN_COMPATIBLE)
-void RequestSuspendOrStop() {
-  if (loader_app::IsPendingRestart()) {
-    SbLogRawFormatF("\nPending update restart . Stopping.\n");
-    SbLogFlush();
-    SbSystemRequestStop(0);
-  } else {
-    SbSystemRequestSuspend();
-  }
-}
-#endif
-
 void Suspend(int signal_id) {
   SignalMask(kAllSignals, SIG_BLOCK);
   LogSignalCaught(signal_id);
-#if SB_IS(EVERGREEN_COMPATIBLE)
-  RequestSuspendOrStop();
-#else
   SbSystemRequestSuspend();
-#endif
   SignalMask(kAllSignals, SIG_UNBLOCK);
 }
 
diff --git a/src/starboard/shared/signal/system_request_suspend.cc b/src/starboard/shared/signal/system_request_suspend.cc
index e76ee39..0ea5fa8 100644
--- a/src/starboard/shared/signal/system_request_suspend.cc
+++ b/src/starboard/shared/signal/system_request_suspend.cc
@@ -17,11 +17,26 @@
 #include "starboard/shared/signal/signal_internal.h"
 #include "starboard/shared/starboard/application.h"
 
+#if SB_IS(EVERGREEN_COMPATIBLE)
+#include "starboard/loader_app/pending_restart.h"
+#endif
+
 void SuspendDone(void* context) {
   // Stop all thread execution after fully transitioning into Suspended.
   raise(SIGSTOP);
 }
 
 void SbSystemRequestSuspend() {
+#if SB_IS(EVERGREEN_COMPATIBLE)
+  if (starboard::loader_app::IsPendingRestart()) {
+    SbLogRawFormatF("\nPending update restart . Stopping.\n");
+    SbLogFlush();
+    starboard::shared::starboard::Application::Get()->Stop(0);
+  } else {
+    starboard::shared::starboard::Application::Get()->Suspend(NULL,
+                                                              &SuspendDone);
+  }
+#else
   starboard::shared::starboard::Application::Get()->Suspend(NULL, &SuspendDone);
+#endif
 }
diff --git a/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h b/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
index bffc40a..c1cf847 100644
--- a/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
+++ b/src/starboard/shared/starboard/audio_sink/audio_sink_internal.h
@@ -27,6 +27,7 @@
   // When |capability_changed| is true, it hints that the error is caused by a
   // a transisent capability on the platform.  The app should retry playback to
   // recover from the error.
+  // TODO: Allow to pass an error message.
   typedef void (*ErrorFunc)(bool capability_changed, void* context);
 #endif  // SB_API_VERSION >= 12
 
diff --git a/src/starboard/shared/starboard/media/media_util.cc b/src/starboard/shared/starboard/media/media_util.cc
index 3269d04..d10b991 100644
--- a/src/starboard/shared/starboard/media/media_util.cc
+++ b/src/starboard/shared/starboard/media/media_util.cc
@@ -623,6 +623,20 @@
   return "Invalid";
 }
 
+#if SB_API_VERSION >= 11
+bool IsAudioSampleInfoSubstantiallyDifferent(
+    const SbMediaAudioSampleInfo& left,
+    const SbMediaAudioSampleInfo& right) {
+  return left.codec != right.codec ||
+         left.samples_per_second != right.samples_per_second ||
+         left.number_of_channels != right.number_of_channels ||
+         left.audio_specific_config_size != right.audio_specific_config_size ||
+         SbMemoryCompare(left.audio_specific_config,
+                         right.audio_specific_config,
+                         left.audio_specific_config_size) != 0;
+}
+#endif  // SB_API_VERSION < 11
+
 }  // namespace media
 }  // namespace starboard
 }  // namespace shared
diff --git a/src/starboard/shared/starboard/media/media_util.h b/src/starboard/shared/starboard/media/media_util.h
index 72edc89..15a9742 100644
--- a/src/starboard/shared/starboard/media/media_util.h
+++ b/src/starboard/shared/starboard/media/media_util.h
@@ -104,6 +104,14 @@
 const char* GetMatrixIdName(SbMediaMatrixId matrix_id);
 const char* GetRangeIdName(SbMediaRangeId range_id);
 
+#if SB_API_VERSION >= 11
+//  When this function returns true, usually indicates that the two sample info
+//  cannot be processed by the same audio decoder.
+bool IsAudioSampleInfoSubstantiallyDifferent(
+    const SbMediaAudioSampleInfo& left,
+    const SbMediaAudioSampleInfo& right);
+#endif  // SB_API_VERSION < 11
+
 }  // namespace media
 }  // namespace starboard
 }  // namespace shared
diff --git a/src/starboard/shared/starboard/player/decoded_audio_internal.cc b/src/starboard/shared/starboard/player/decoded_audio_internal.cc
index 1e21a40..aca60f5 100644
--- a/src/starboard/shared/starboard/player/decoded_audio_internal.cc
+++ b/src/starboard/shared/starboard/player/decoded_audio_internal.cc
@@ -81,10 +81,11 @@
 
   if (samples_per_second == 0 || frames_to_remove < 0 ||
       frames_to_remove >= frames()) {
-    SB_LOG(WARNING) << "AdjustForSeekTime failed for seeking_to_time at "
-                    << seeking_to_time << " for samples_per_second "
-                    << samples_per_second << ", and there are " << frames()
-                    << " frames in the DecodedAudio object.";
+    SB_LOG(WARNING) << "AdjustForSeekTime failed for seeking_to_time: "
+                    << seeking_to_time
+                    << ", samples_per_second: " << samples_per_second
+                    << ", timestamp: " << timestamp() << ", and there are "
+                    << frames() << " frames in the DecodedAudio object.";
     return;
   }
 
diff --git a/src/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc b/src/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
index ae3b448..8eba0ca 100644
--- a/src/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
+++ b/src/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
@@ -17,6 +17,7 @@
 #include "starboard/audio_sink.h"
 #include "starboard/common/log.h"
 #include "starboard/common/reset_and_return.h"
+#include "starboard/shared/starboard/media/media_util.h"
 #include "starboard/shared/starboard/player/decoded_audio_internal.h"
 
 namespace starboard {
@@ -34,29 +35,6 @@
       kDefaultOutputSamplesPerSecond);
 }
 
-bool IsResetDecoderNecessary(const SbMediaAudioSampleInfo& current_info,
-                             const SbMediaAudioSampleInfo& new_info) {
-  if (current_info.codec != new_info.codec) {
-    return true;
-  }
-  if (current_info.samples_per_second != new_info.samples_per_second) {
-    return true;
-  }
-  if (current_info.number_of_channels != new_info.number_of_channels) {
-    return true;
-  }
-  if (current_info.audio_specific_config_size !=
-      new_info.audio_specific_config_size) {
-    return true;
-  }
-  if (SbMemoryCompare(current_info.audio_specific_config,
-                      new_info.audio_specific_config,
-                      current_info.audio_specific_config_size) != 0) {
-    return true;
-  }
-  return false;
-}
-
 AdaptiveAudioDecoder::AdaptiveAudioDecoder(
     const SbMediaAudioSampleInfo& audio_sample_info,
     SbDrmSystem drm_system,
@@ -107,8 +85,8 @@
     }
     return;
   }
-  if (IsResetDecoderNecessary(input_audio_sample_info_,
-                              input_buffer->audio_sample_info())) {
+  if (starboard::media::IsAudioSampleInfoSubstantiallyDifferent(
+          input_audio_sample_info_, input_buffer->audio_sample_info())) {
     flushing_ = true;
     pending_input_buffer_ = input_buffer;
     pending_consumed_cb_ = consumed_cb;
diff --git a/src/starboard/shared/stub/mutex_acquire.cc b/src/starboard/shared/stub/mutex_acquire.cc
index cede4d1..423202e 100644
--- a/src/starboard/shared/stub/mutex_acquire.cc
+++ b/src/starboard/shared/stub/mutex_acquire.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 SbMutexResult SbMutexAcquire(SbMutex* mutex) {
   return kSbMutexDestroyed;
diff --git a/src/starboard/shared/stub/mutex_acquire_try.cc b/src/starboard/shared/stub/mutex_acquire_try.cc
index a9f1ba8..efc87b6 100644
--- a/src/starboard/shared/stub/mutex_acquire_try.cc
+++ b/src/starboard/shared/stub/mutex_acquire_try.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 SbMutexResult SbMutexAcquireTry(SbMutex* mutex) {
   return kSbMutexDestroyed;
diff --git a/src/starboard/shared/stub/mutex_create.cc b/src/starboard/shared/stub/mutex_create.cc
index 11586de..4439ee0 100644
--- a/src/starboard/shared/stub/mutex_create.cc
+++ b/src/starboard/shared/stub/mutex_create.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 bool SbMutexCreate(SbMutex* mutex) {
   return false;
diff --git a/src/starboard/shared/stub/mutex_destroy.cc b/src/starboard/shared/stub/mutex_destroy.cc
index 7922ddd..b88ae22 100644
--- a/src/starboard/shared/stub/mutex_destroy.cc
+++ b/src/starboard/shared/stub/mutex_destroy.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 bool SbMutexDestroy(SbMutex* mutex) {
   return false;
diff --git a/src/starboard/shared/stub/mutex_release.cc b/src/starboard/shared/stub/mutex_release.cc
index 88e6071..207b642 100644
--- a/src/starboard/shared/stub/mutex_release.cc
+++ b/src/starboard/shared/stub/mutex_release.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "starboard/common/mutex.h"
+#include "starboard/mutex.h"
 
 bool SbMutexRelease(SbMutex* mutex) {
   return false;
diff --git a/src/starboard/starboard.gyp b/src/starboard/starboard.gyp
index 0808490..9c74ba2 100644
--- a/src/starboard/starboard.gyp
+++ b/src/starboard/starboard.gyp
@@ -19,7 +19,7 @@
 {
   'targets': [
     {
-      'target_name': 'starboard_base',
+      'target_name': 'starboard',
       'type': 'none',
       'conditions': [
         ['sb_evergreen == 1', {
@@ -37,14 +37,6 @@
             'starboard_full',
           ],
         }],
-       ],
-    },
-    {
-      'target_name': 'starboard',
-      'type': 'none',
-      'dependencies': [
-        '<(DEPTH)/third_party/crashpad/wrapper/wrapper.gyp:crashpad_wrapper_stub',
-        'starboard_base',
       ],
     },
     {
@@ -60,6 +52,15 @@
         '<(DEPTH)/<(starboard_path)/starboard_platform.gyp:starboard_platform',
       ],
       'conditions': [
+        ['sb_crashpad_enabled == 1', {
+          'dependencies': [
+            '<(DEPTH)/third_party/crashpad/wrapper/wrapper.gyp:crashpad_wrapper',
+          ],
+        }, {
+          'dependencies': [
+            '<(DEPTH)/third_party/crashpad/wrapper/wrapper.gyp:crashpad_wrapper_stub',
+          ],
+        }],
         ['final_executable_type=="shared_library"', {
           'all_dependent_settings': {
             'target_conditions': [
diff --git a/src/starboard/stub/configuration.cc b/src/starboard/stub/configuration.cc
index 6af5309..1f9a6dd 100644
--- a/src/starboard/stub/configuration.cc
+++ b/src/starboard/stub/configuration.cc
@@ -28,7 +28,7 @@
 
 const CobaltExtensionConfigurationApi kConfigurationApi = {
     kCobaltExtensionConfigurationName,
-    1,
+    2,
     &common::CobaltUserOnExitStrategyDefault,
     &common::CobaltRenderDirtyRegionOnlyDefault,
     &common::CobaltEglSwapIntervalDefault,
@@ -51,6 +51,7 @@
     &common::CobaltGcZealDefault,
     &CobaltRasterizerType,
     &common::CobaltEnableJitDefault,
+    &common::CobaltFallbackSplashScreenTopicsDefault,
 };
 
 }  // namespace
diff --git a/src/starboard/stub/configuration_public.h b/src/starboard/stub/configuration_public.h
index f27313c..cc64e9d 100644
--- a/src/starboard/stub/configuration_public.h
+++ b/src/starboard/stub/configuration_public.h
@@ -22,12 +22,6 @@
 #ifndef STARBOARD_STUB_CONFIGURATION_PUBLIC_H_
 #define STARBOARD_STUB_CONFIGURATION_PUBLIC_H_
 
-#if SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-#error \
-    "This platform's sabi.json file is expected to track the experimental " \
-"Starboard API version."
-#endif  // SB_API_VERSION != SB_EXPERIMENTAL_API_VERSION
-
 // --- Architecture Configuration --------------------------------------------
 
 // Some platforms will not align variables on the stack with an alignment
diff --git a/src/starboard/tools/abstract_launcher.py b/src/starboard/tools/abstract_launcher.py
index 1ba0165..51a9d3c 100644
--- a/src/starboard/tools/abstract_launcher.py
+++ b/src/starboard/tools/abstract_launcher.py
@@ -23,6 +23,10 @@
 from starboard.tools import build
 from starboard.tools import paths
 
+ARG_NOINSTALL = "noinstall"
+ARG_SYSTOOLS = "systools"
+ARG_DRYRUN = "dryrun"
+
 
 def _GetLauncherForPlatform(platform_name):
   """Gets the module containing a platform's concrete launcher implementation.
@@ -125,6 +129,11 @@
       env_variables = {}
     self.env_variables = env_variables
 
+    launcher_args = kwargs.get("launcher_args", None)
+    if launcher_args is None:
+      launcher_args = []
+    self.launcher_args = launcher_args
+
     # Launchers that need different startup timeout times should reassign
     # this variable during initialization.
     self.startup_timeout_seconds = 2 * 60
diff --git a/src/starboard/tools/app_launcher_packager.py b/src/starboard/tools/app_launcher_packager.py
index 9090373..ee8ade8 100644
--- a/src/starboard/tools/app_launcher_packager.py
+++ b/src/starboard/tools/app_launcher_packager.py
@@ -24,6 +24,7 @@
 import logging
 import os
 import shutil
+import string
 import sys
 import tempfile
 
@@ -32,21 +33,18 @@
 from paths import THIRD_PARTY_ROOT
 sys.path.append(THIRD_PARTY_ROOT)
 # pylint: disable=g-import-not-at-top,g-bad-import-order
-import jinja2
 from starboard.tools import port_symlink
 import starboard.tools.platform
 
 # Default python directories to app launcher resources.
 _INCLUDE_FILE_PATTERNS = [
-    ('buildbot', '*.py'),
+    ('buildbot', '_env.py'),  # Only needed for device_server to execute
+    ('buildbot', '__init__.py'),  # Only needed for device_server to execute
+    ('buildbot/device_server', '*.py'),
     ('buildbot/device_server/shared/ssl_certs', '*'),
     ('cobalt', '*.py'),
-    # TODO: Test and possibly prune.
-    ('lbshell', '*.py'),
     ('starboard', '*.py'),
-    # jinja2 required by this app_launcher_packager.py script.
-    ('third_party/jinja2', '*.py'),
-    ('third_party/markupsafe', '*.py'),  # Required by third_party/jinja2
+    ('starboard/tools', 'platform.py.template')
 ]
 
 _INCLUDE_BLACK_BOX_TESTS_PATTERNS = [
@@ -124,7 +122,7 @@
   logging.info('Baking platform info files.')
   current_file = os.path.abspath(__file__)
   current_dir = os.path.dirname(current_file)
-  dest_dir = current_dir.replace(repo_root, dest_root)
+  dest_dir = os.path.join(dest_root, 'starboard', 'tools')
   platforms_map = {}
   for p in starboard.tools.platform.GetAll():
     platform_path = os.path.relpath(
@@ -132,10 +130,11 @@
     # Store posix paths even on Windows so MH Linux hosts can use them.
     # The template has code to re-normalize them when used on Windows hosts.
     platforms_map[p] = platform_path.replace('\\', '/')
-  template = jinja2.Template(
+  template = string.Template(
       open(os.path.join(current_dir, 'platform.py.template')).read())
   with open(os.path.join(dest_dir, 'platform.py'), 'w+') as f:
-    template.stream(platforms_map=platforms_map).dump(f, encoding='utf-8')
+    sub = template.substitute(platforms_map=platforms_map)
+    f.write(sub.encode('utf-8'))
   logging.info('Finished baking in platform info files.')
 
 
@@ -257,7 +256,11 @@
       help='List to stdout the application resources relative to the current '
       'directory.')
   parser.add_argument(
-      '-v', '--verbose', action='store_true', help='Verbose logging output.')
+      '-v',
+      '--verbose',
+      action='store_true',
+      help='Enables verbose logging. For more control over the '
+      "logging level use '--log_level' instead.")
   args = parser.parse_args(command_args)
 
   if not args.verbose:
diff --git a/src/starboard/tools/platform.py.template b/src/starboard/tools/platform.py.template
index 17d55b5..204d415 100644
--- a/src/starboard/tools/platform.py.template
+++ b/src/starboard/tools/platform.py.template
@@ -20,7 +20,7 @@
 from starboard.tools import environment
 
 # The name->platform path mapping.
-_PATH_MAP = {{platforms_map}}
+_PATH_MAP = $platforms_map
 _PATH_MAP = {k:os.path.normpath(v) for k,v in _PATH_MAP.iteritems()}
 
 
diff --git a/src/starboard/tools/testing/test_runner.py b/src/starboard/tools/testing/test_runner.py
index e6ca771..562bb25 100755
--- a/src/starboard/tools/testing/test_runner.py
+++ b/src/starboard/tools/testing/test_runner.py
@@ -218,7 +218,8 @@
                application_name=None,
                dry_run=False,
                xml_output_dir=None,
-               log_xml_results=False):
+               log_xml_results=False,
+               launcher_args=None):
     self.platform = platform
     self.config = config
     self.loader_platform = loader_platform
@@ -227,6 +228,7 @@
     self.target_params = target_params
     self.out_directory = out_directory
     self.loader_out_directory = loader_out_directory
+    self.launcher_args = launcher_args
     if not self.out_directory:
       self.out_directory = paths.BuildOutputDirectory(self.platform,
                                                       self.config)
@@ -336,6 +338,7 @@
     return final_targets
 
   def _GetTestFilters(self):
+    """Get test filters for a given platform and configuration."""
     filters = self._platform_config.GetTestFilters()
     app_filters = self._app_config.GetTestFilters()
     if app_filters:
@@ -348,10 +351,10 @@
       loader_platform_config = build.GetPlatformConfig(self.loader_platform)
       loader_app_config = loader_platform_config.GetApplicationConfiguration(
           self.application_name)
-      for filter in (loader_platform_config.GetTestFilters() +
-                     loader_app_config.GetTestFilters()):
-        if filter not in filters:
-          filters.append(filter)
+      for filter_ in (loader_platform_config.GetTestFilters() +
+                      loader_app_config.GetTestFilters()):
+        if filter_ not in filters:
+          filters.append(filter_)
     return filters
 
   def _GetAllTestEnvVariables(self):
@@ -430,7 +433,8 @@
         env_variables=env,
         loader_platform=self.loader_platform,
         loader_config=self.loader_config,
-        loader_out_directory=self.loader_out_directory)
+        loader_out_directory=self.loader_out_directory,
+        launcher_args=self.launcher_args)
 
     test_reader = TestLineReader(read_pipe)
     test_launcher = TestLauncher(launcher)
@@ -547,10 +551,10 @@
     total_flaky_failed_count = 0
     total_filtered_count = 0
 
-    print  # Explicit print for empty formatting line.
+    print()  # Explicit print for empty formatting line.
     logging.info("TEST RUN COMPLETE.")
     if results:
-      print  # Explicit print for empty formatting line.
+      print()  # Explicit print for empty formatting line.
 
     # If the number of run tests from a test binary cannot be
     # determined, assume an error occurred while running it.
@@ -594,7 +598,7 @@
             # Sometimes the returned test "name" includes information about the
             # parameter that was passed to it. This needs to be stripped off.
             retry_result = self._RunTest(target_name, test_case.split(",")[0])
-            print  # Explicit print for empty formatting line.
+            print()  # Explicit print for empty formatting line.
             if retry_result[2] == 1:
               flaky_passed_tests.append(test_case)
               logging.info("%s succeeded on run #%d!\n", test_case, retry + 2)
@@ -707,11 +711,12 @@
       # tests so we need to build it separately.
       if self.loader_platform:
         build_tests.BuildTargets(
-            [_LOADER_TARGET, _CRASHPAD_TARGET], self.loader_out_directory, self.dry_run,
-            extra_flags + [os.getenv('TEST_RUNNER_PLATFORM_BUILD_FLAGS', '')])
+            [_LOADER_TARGET, _CRASHPAD_TARGET], self.loader_out_directory,
+            self.dry_run,
+            extra_flags + [os.getenv("TEST_RUNNER_PLATFORM_BUILD_FLAGS", "")])
       build_tests.BuildTargets(
           self.test_targets, self.out_directory, self.dry_run,
-          extra_flags + [os.getenv('TEST_RUNNER_BUILD_FLAGS', '')])
+          extra_flags + [os.getenv("TEST_RUNNER_BUILD_FLAGS", "")])
 
     except subprocess.CalledProcessError as e:
       result = False
@@ -827,6 +832,13 @@
       action="store_true",
       help="If set, results will be logged in xml format after all tests are"
       " complete. --xml_output_dir will be ignored.")
+  arg_parser.add_argument(
+      "-w",
+      "--launcher_args",
+      help="Pass space-separated arguments to control launcher behaviour. "
+      "Arguments are plaform specific and may not be implemented for all "
+      "platforms. Common arguments are:\n\t'noinstall' - skip install steps "
+      "before running the test\n\t'systools' - use system-installed tools.")
   args = arg_parser.parse_args()
 
   if (args.loader_platform and not args.loader_config or
@@ -840,12 +852,19 @@
   if args.target_params:
     target_params = args.target_params.split(" ")
 
+  launcher_args = []
+  if args.launcher_args:
+    launcher_args = args.launcher_args.split(" ")
+
+  if args.dry_run:
+    launcher_args.append(abstract_launcher.ARG_DRYRUN)
+
   runner = TestRunner(args.platform, args.config, args.loader_platform,
                       args.loader_config, args.device_id, args.target_name,
                       target_params, args.out_directory,
                       args.loader_out_directory, args.platform_tests_only,
                       args.application_name, args.dry_run, args.xml_output_dir,
-                      args.log_xml_results)
+                      args.log_xml_results, launcher_args)
 
   def Abort(signum, frame):
     del signum, frame  # Unused.
diff --git a/src/starboard/tools/toolchain/cmd.py b/src/starboard/tools/toolchain/cmd.py
index 15a7963..cb30844 100644
--- a/src/starboard/tools/toolchain/cmd.py
+++ b/src/starboard/tools/toolchain/cmd.py
@@ -27,6 +27,10 @@
 class Shell(abstract.Shell):
   """Constructs command lines using Cmd syntax."""
 
+  def __init__(self, quote=True):
+    # Toggle whether or not to quote command line arguments.
+    self.quote = quote
+
   def MaybeQuoteArgument(self, arg):
     # Rather than attempting to enumerate the bad shell characters, just
     # whitelist common OK ones and quote anything else.
@@ -76,7 +80,10 @@
 
   def Join(self, command):
     assert not isinstance(command, basestring)
-    return ' '.join(self.MaybeQuoteArgument(argument) for argument in command)
+    if self.quote:
+      return ' '.join(self.MaybeQuoteArgument(argument) for argument in command)
+    else:
+      return ' '.join(command)
 
   def And(self, *commands):
     return ' && '.join(_MaybeJoin(self, command) for command in commands)
diff --git a/src/starboard/tools/toolchain/python.py b/src/starboard/tools/toolchain/python.py
index bee8841..0f32d88 100644
--- a/src/starboard/tools/toolchain/python.py
+++ b/src/starboard/tools/toolchain/python.py
@@ -11,18 +11,19 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""Allow to use gyp-win-tool via python as a copy and a stamp tools."""
+"""Allow use of gyp-win-tool via python as copy and stamp tools."""
 
 import sys
 from starboard.tools.toolchain import abstract
 
 
 class Copy(abstract.Copy):
-  """Copies individual files using python.exe."""
+  """Copies individual files."""
 
   def __init__(self, **kwargs):
     self._path = kwargs.get('path', sys.executable)
-    self._extra_flags = kwargs.get('extra_flags', [])
+    self._extra_flags = kwargs.get('extra_flags',
+                                   ['gyp-win-tool', 'recursive-mirror'])
 
   def GetPath(self):
     return self._path
@@ -55,7 +56,7 @@
 
   def __init__(self, **kwargs):
     self._path = kwargs.get('path', sys.executable)
-    self._extra_flags = kwargs.get('extra_flags', [])
+    self._extra_flags = kwargs.get('extra_flags', ['gyp-win-tool', 'stamp'])
 
   def GetPath(self):
     return self._path
diff --git a/src/third_party/angle/include/platform/Platform.h b/src/third_party/angle/include/platform/Platform.h
index 09505a3..1035f8e 100644
--- a/src/third_party/angle/include/platform/Platform.h
+++ b/src/third_party/angle/include/platform/Platform.h
@@ -236,11 +236,11 @@
 using ProgramKeyType   = std::array<uint8_t, 20>;
 using CacheProgramFunc = void (*)(PlatformMethods *platform,
                                   const ProgramKeyType &key,
-                                  size_t programSize,
+                                  std::size_t programSize,
                                   const uint8_t *programBytes);
 inline void DefaultCacheProgram(PlatformMethods *platform,
                                 const ProgramKeyType &key,
-                                size_t programSize,
+                                std::size_t programSize,
                                 const uint8_t *programBytes)
 {}
 
diff --git a/src/third_party/crashpad/util/file/file_io.h b/src/third_party/crashpad/util/file/file_io.h
index 6fa0f96..46b1ee6 100644
--- a/src/third_party/crashpad/util/file/file_io.h
+++ b/src/third_party/crashpad/util/file/file_io.h
@@ -19,6 +19,7 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "build/build_config.h"
 
 #if defined(OS_POSIX)
diff --git a/src/third_party/crashpad/util/net/http_transport_socket.cc b/src/third_party/crashpad/util/net/http_transport_socket.cc
index 5a88ccd..278a8b8 100644
--- a/src/third_party/crashpad/util/net/http_transport_socket.cc
+++ b/src/third_party/crashpad/util/net/http_transport_socket.cc
@@ -144,7 +144,12 @@
                            kSbFileSepString + "cobalt" + kSbFileSepString +
                            "content" + kSbFileSepString + "ssl" +
                            kSbFileSepString + "certs");
-
+      // If this is not Cobalt Evergreen setup use the regular content path.
+      if (!SbFileExists(cert_location.c_str())) {
+        cert_location = buffer.data();
+        cert_location.append(std::string(kSbFileSepString) + "ssl" +
+                             kSbFileSepString + "certs");
+      }
       if (SSL_CTX_load_verify_locations(
               ctx_.get(), nullptr, cert_location.c_str()) <= 0) {
         LOG(ERROR) << "SSL_CTX_load_verify_locations";
diff --git a/src/third_party/crashpad/wrapper/wrapper.cc b/src/third_party/crashpad/wrapper/wrapper.cc
index 0ab09e5..ef36040 100644
--- a/src/third_party/crashpad/wrapper/wrapper.cc
+++ b/src/third_party/crashpad/wrapper/wrapper.cc
@@ -25,6 +25,7 @@
 #include "client/settings.h"
 #include "starboard/configuration_constants.h"
 #include "starboard/directory.h"
+#include "starboard/file.h"
 #include "starboard/system.h"
 
 namespace third_party {
@@ -179,6 +180,12 @@
   ::crashpad::CrashpadClient* client = GetCrashpadClient();
 
   const base::FilePath handler_path = GetPathToCrashpadHandlerBinary();
+  if (!SbFileExists(handler_path.value().c_str())) {
+    LOG(WARNING) << "crashpad_handler not at expected location of "
+                 << handler_path.value();
+    return;
+  }
+
   const base::FilePath database_directory_path = GetDatabasePath();
   const base::FilePath default_metrics_dir;
   const std::string product_name = GetProductName();
diff --git a/src/third_party/crashpad/wrapper/wrapper.gyp b/src/third_party/crashpad/wrapper/wrapper.gyp
index a9a4382..b7196e1 100644
--- a/src/third_party/crashpad/wrapper/wrapper.gyp
+++ b/src/third_party/crashpad/wrapper/wrapper.gyp
@@ -22,22 +22,16 @@
         'wrapper.h',
       ],
     },
-  ],
-  'conditions': [
-    ['sb_evergreen_compatible == 1', {
-      'targets': [
-        {
-          'target_name': 'crashpad_wrapper',
-          'type': 'static_library',
-          'sources': [
-            'wrapper.cc',
-            'wrapper.h',
-          ],
-          'dependencies': [
-            '<(DEPTH)/third_party/crashpad/client/client.gyp:crashpad_client',
-          ],
-        },
+    {
+      'target_name': 'crashpad_wrapper',
+      'type': 'static_library',
+      'sources': [
+        'wrapper.cc',
+        'wrapper.h',
       ],
-    }],
+      'dependencies': [
+        '<(DEPTH)/third_party/crashpad/client/client.gyp:crashpad_client',
+      ],
+    },
   ],
 }
diff --git a/src/third_party/freetype2/ChangeLog b/src/third_party/freetype2/ChangeLog
index e4ea3c5..921d93e 100644
--- a/src/third_party/freetype2/ChangeLog
+++ b/src/third_party/freetype2/ChangeLog
@@ -1,3 +1,11 @@
+2020-10-19  Werner Lemberg  <wl@gnu.org>
+
+       [sfnt] Fix heap buffer overflow (#59308).
+
+       This is CVE-2020-15999.
+
+       * src/sfnt/pngshim.c (Load_SBit_Png): Test bitmap size earlier.
+
 2020-05-09  Werner Lemberg  <wl@gnu.org>
 
 	* Version 2.10.2 released.
diff --git a/src/third_party/freetype2/src/sfnt/pngshim.c b/src/third_party/freetype2/src/sfnt/pngshim.c
index 523b30a..5502108 100644
--- a/src/third_party/freetype2/src/sfnt/pngshim.c
+++ b/src/third_party/freetype2/src/sfnt/pngshim.c
@@ -328,6 +328,13 @@
 
     if ( populate_map_and_metrics )
     {
+      /* reject too large bitmaps similarly to the rasterizer */
+      if ( imgHeight > 0x7FFF || imgWidth > 0x7FFF )
+      {
+        error = FT_THROW( Array_Too_Large );
+        goto DestroyExit;
+      }
+
       metrics->width  = (FT_UShort)imgWidth;
       metrics->height = (FT_UShort)imgHeight;
 
@@ -336,13 +343,6 @@
       map->pixel_mode = FT_PIXEL_MODE_BGRA;
       map->pitch      = (int)( map->width * 4 );
       map->num_grays  = 256;
-
-      /* reject too large bitmaps similarly to the rasterizer */
-      if ( map->rows > 0x7FFF || map->width > 0x7FFF )
-      {
-        error = FT_THROW( Array_Too_Large );
-        goto DestroyExit;
-      }
     }
 
     /* convert palette/gray image to rgb */
diff --git a/src/third_party/mini_chromium/base/base.gyp b/src/third_party/mini_chromium/base/base.gyp
index 7d1dbee..71dd510 100644
--- a/src/third_party/mini_chromium/base/base.gyp
+++ b/src/third_party/mini_chromium/base/base.gyp
@@ -43,6 +43,7 @@
         'atomicops_internals_atomicword_compat.h',
         'atomicops_internals_portable.h',
         'auto_reset.h',
+        'base_wrapper.h',
         'bit_cast.h',
         'compiler_specific.h',
         'debug/alias.cc',
diff --git a/src/third_party/mini_chromium/base/base_wrapper.h b/src/third_party/mini_chromium/base/base_wrapper.h
new file mode 100644
index 0000000..ae57c30
--- /dev/null
+++ b/src/third_party/mini_chromium/base/base_wrapper.h
@@ -0,0 +1,29 @@
+// Copyright 2020 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
+#define MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
+
+// Change the symbol name to avoid collisions with //base
+#define FilePath MFilePath
+#define GetLogMessageHandler MGetLogMessageHandler
+#define LogMessage MLogMessage
+#define ReadUnicodeCharacter MReadUnicodeCharacter
+#define SetLogMessageHandler MSetLogMessageHandler
+#define UTF16ToUTF8 MUTF16ToUTF8
+#define UmaHistogramSparse MUmaHistogramSparse
+#define WriteUnicodeCharacter MWriteUnicodeCharacter
+#define c16len mc16len
+
+#endif  // MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
diff --git a/src/third_party/mini_chromium/base/files/file_path.h b/src/third_party/mini_chromium/base/files/file_path.h
index 0ac91bf..ca20380 100644
--- a/src/third_party/mini_chromium/base/files/file_path.h
+++ b/src/third_party/mini_chromium/base/files/file_path.h
@@ -107,6 +107,7 @@
 #include <iosfwd>
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "base/compiler_specific.h"
 #include "build/build_config.h"
 
diff --git a/src/third_party/mini_chromium/base/logging.h b/src/third_party/mini_chromium/base/logging.h
index 37bf23b..0270090 100644
--- a/src/third_party/mini_chromium/base/logging.h
+++ b/src/third_party/mini_chromium/base/logging.h
@@ -12,6 +12,7 @@
 #include <sstream>
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "base/macros.h"
 #include "build/build_config.h"
 
diff --git a/src/third_party/mini_chromium/base/metrics/histogram_functions.h b/src/third_party/mini_chromium/base/metrics/histogram_functions.h
index a96cb9d..5f2af8f 100644
--- a/src/third_party/mini_chromium/base/metrics/histogram_functions.h
+++ b/src/third_party/mini_chromium/base/metrics/histogram_functions.h
@@ -7,6 +7,8 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
+
 // These are no-op stub versions of a subset of the functions from Chromium's
 // base/metrics/histogram_functions.h. This allows us to instrument the Crashpad
 // code as necessary, while not affecting out-of-Chromium builds.
diff --git a/src/third_party/mini_chromium/base/strings/string16.h b/src/third_party/mini_chromium/base/strings/string16.h
index 04605e4..bb311c2 100644
--- a/src/third_party/mini_chromium/base/strings/string16.h
+++ b/src/third_party/mini_chromium/base/strings/string16.h
@@ -10,6 +10,7 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "build/build_config.h"
 
 namespace base {
diff --git a/src/third_party/mini_chromium/base/strings/utf_string_conversion_utils.h b/src/third_party/mini_chromium/base/strings/utf_string_conversion_utils.h
index 72f86ae..4d5353d 100644
--- a/src/third_party/mini_chromium/base/strings/utf_string_conversion_utils.h
+++ b/src/third_party/mini_chromium/base/strings/utf_string_conversion_utils.h
@@ -5,6 +5,7 @@
 #ifndef MINI_CHROMIUM_BASE_STRINGS_UTF_STRING_CONVERSION_UTILS_H_
 #define MINI_CHROMIUM_BASE_STRINGS_UTF_STRING_CONVERSION_UTILS_H_
 
+#include "base/base_wrapper.h"
 #include "base/strings/string16.h"
 
 namespace base {
diff --git a/src/third_party/mini_chromium/base/strings/utf_string_conversions.h b/src/third_party/mini_chromium/base/strings/utf_string_conversions.h
index c2ca674..8e97fcd 100644
--- a/src/third_party/mini_chromium/base/strings/utf_string_conversions.h
+++ b/src/third_party/mini_chromium/base/strings/utf_string_conversions.h
@@ -7,6 +7,7 @@
 
 #include <string>
 
+#include "base/base_wrapper.h"
 #include "base/strings/string16.h"
 #include "base/strings/string_piece.h"
 
diff --git a/src/third_party/web_platform_tests/cobalt_special/resources/content_length.py b/src/third_party/web_platform_tests/cobalt_special/resources/content_length.py
new file mode 100644
index 0000000..d4282ce
--- /dev/null
+++ b/src/third_party/web_platform_tests/cobalt_special/resources/content_length.py
@@ -0,0 +1,14 @@
+# Lint as: python3
+"""
+Helper script to send XHR response with specified content length.
+"""
+
+def main(request, response):
+  headers = []
+
+  content_length = request.GET.first("content_length")
+  headers.append(("Content-Length", content_length));
+
+  body = "this is body"
+
+  return headers, body
diff --git a/src/third_party/web_platform_tests/cobalt_special/xhr_content_length.htm b/src/third_party/web_platform_tests/cobalt_special/xhr_content_length.htm
new file mode 100644
index 0000000..fc07412
--- /dev/null
+++ b/src/third_party/web_platform_tests/cobalt_special/xhr_content_length.htm
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>cobalt-special - xhr content length</title>
+
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/common/utils.js></script>
+<script src=resources/support.js?pipe=sub></script>
+
+<h1></h1>
+
+<div id=log></div>
+<script>
+  var test = async_test();
+      test.step(function() {
+        var client = new XMLHttpRequest();
+        client.onloadstart = test.step_func(function() {
+          test.done();
+        });
+        // The purpose of this test is to verify that Cobalt does not crash when an
+        // erroneously long content length is specified in xhr response.
+        const too_large_buffer_size = "4987398465139";
+        client.open("GET", "resources/content_length.py?content_length=" + too_large_buffer_size);
+        client.send(null);
+      });
+</script>
+
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
index 22353e3..51ddeff 100644
--- a/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
@@ -5,13 +5,18 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
 }
 .target {
   width: 100px;
@@ -22,7 +27,7 @@
 </style>
 
 <div class="spacer"></div>
-<div id="target1" class="target"></div>
+<div id="target1" class="target" style="margin-top:100px;"></div>
 <div id="target2" class="target"></div>
 <div id="target3" class="target"></div>
 
@@ -38,7 +43,8 @@
   target3 = document.getElementById("target3");
   assert_true(!!target3, "target3 exists.");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   });
   observer.observe(target1);
   observer.observe(target2);
@@ -47,9 +53,12 @@
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF.");
 }, "One observer with multiple targets.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 150;
+  target1.style.marginTop = "-50px";
+  //document.scrollingElement.scrollTop = 150;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 150");
   assert_equals(entries.length, 3, "Three initial notifications.");
   assert_equals(entries[0].target, target1, "entries[0].target === target1");
@@ -58,14 +67,19 @@
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 10000;
+  // document.scrollingElement.scrollTop = 10000 indicates that the page should
+  // scroll just far down enough to include all the elements in the DOM tree.
+  // Since we are using marginTop as a workaround, we need to adjust the value.
+  target1.style.marginTop = "-330px";
+  //document.scrollingElement.scrollTop = 10000;
   runTestCycle(step2, "document.scrollingElement.scrollTop = 10000");
   assert_equals(entries.length, 4, "Four notifications.");
   assert_equals(entries[3].target, target1, "entries[3].target === target1");
 }
 
 function step2() {
-  document.scrollingElement.scrollTop = 0;
+  target1.style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
   runTestCycle(step3, "document.scrollingElement.scrollTop = 0");
   assert_equals(entries.length, 6, "Six notifications.");
   assert_equals(entries[4].target, target2, "entries[4].target === target2");
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
index 3599e1f..3c92ea5 100644
--- a/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
@@ -5,18 +5,26 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
+
 }
 #target {
   width: 100px;
   height: 100px;
   background-color: green;
+  /* Adjust for subtracted 100px in class=spacer */
+  margin-top: 100px;
 }
 </style>
 
@@ -25,8 +33,8 @@
 <div class="spacer"></div>
 
 <script>
-var vw = document.documentElement.clientWidth;
-var vh = document.documentElement.clientHeight;
+var vw = window.innerWidth;
+var vh = window.innerHeight;
 
 var entries = [];
 var target;
@@ -34,64 +42,80 @@
 runTestCycle(function() {
   target = document.getElementById("target");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   }, { threshold: [0, 0.25, 0.5, 0.75, 1] });
   observer.observe(target);
   entries = entries.concat(observer.takeRecords());
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF.");
 }, "Observer with multiple thresholds.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 120;
+  target.style.marginTop = "-20px";
+  //document.scrollingElement.scrollTop = 120;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 120");
   checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 160;
+  target.style.marginTop = "-60px";
+  //document.scrollingElement.scrollTop = 160;
   runTestCycle(step2, "document.scrollingElement.scrollTop = 160");
   checkLastEntry(entries, 1, [8, 108, vh - 12, vh + 88, 8, 108, vh - 12, vh, 0, vw, 0, vh, true]);
 }
 
 function step2() {
-  document.scrollingElement.scrollTop = 200;
+  target.style.marginTop = "-100px";
+  //document.scrollingElement.scrollTop = 200;
   runTestCycle(step3, "document.scrollingElement.scrollTop = 200");
   checkLastEntry(entries, 2, [8, 108, vh - 52, vh + 48, 8, 108, vh - 52, vh, 0, vw, 0, vh, true]);
 }
 
 function step3() {
-  document.scrollingElement.scrollTop = 240;
+  target.style.marginTop = "-140px"; 
+  //document.scrollingElement.scrollTop = 240;
   runTestCycle(step4, "document.scrollingElement.scrollTop = 240");
   checkLastEntry(entries, 3, [8, 108, vh - 92, vh + 8, 8, 108, vh - 92, vh, 0, vw, 0, vh, true]);
 }
 
 function step4() {
-  document.scrollingElement.scrollTop = vh + 140;
+  var marginTop = (vh + 140) * -1 + 100 + "px";
+  target.style.marginTop = marginTop;
+  //document.scrollingElement.scrollTop = vh + 140;
   runTestCycle(step5, "document.scrollingElement.scrollTop = window.innerHeight + 140");
   checkLastEntry(entries, 4, [8, 108, vh - 132, vh - 32, 8, 108, vh - 132, vh - 32, 0, vw, 0, vh, true]);
 }
 
 function step5() {
-  document.scrollingElement.scrollTop = vh + 160;
+  var marginTop = (vh + 160) * -1 + 100 + "px";
+  target.style.marginTop = marginTop;
+  //document.scrollingElement.scrollTop = vh + 160;
   runTestCycle(step6, "document.scrollingElement.scrollTop = window.innerHeight + 160");
   checkLastEntry(entries, 5, [8, 108, -32, 68, 8, 108, 0, 68, 0, vw, 0, vh, true]);
 }
 
 function step6() {
-  document.scrollingElement.scrollTop = vh + 200;
+  var marginTop = (vh + 200) * -1 + 100 + "px";
+  target.style.marginTop = marginTop;
+  //document.scrollingElement.scrollTop = vh + 200;
   runTestCycle(step7, "document.scrollingElement.scrollTop = window.innerHeight + 200");
   checkLastEntry(entries, 6, [8, 108, -52, 48, 8, 108, 0, 48, 0, vw, 0, vh, true]);
 }
 
 function step7() {
   checkLastEntry(entries, 7, [8, 108, -92, 8, 8, 108, 0, 8, 0, vw, 0, vh, true]);
-  document.scrollingElement.scrollTop = vh + 220;
+  var marginTop = (vh + 220) * -1 + 100 + "px";
+  target.style.marginTop = marginTop;
+  //document.scrollingElement.scrollTop = vh + 220;
   runTestCycle(step8, "document.scrollingElement.scrollTop = window.innerHeight + 220");
 }
 
 function step8() {
   checkLastEntry(entries, 8, [8, 108, -112, -12, 0, 0, 0, 0, 0, vw, 0, vh, false]);
-  document.scrollingElement.scrollTop = 0;
+  target.style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
 }
 </script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
index 53100c50..afb28c6 100644
--- a/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
@@ -5,18 +5,25 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
 }
 #target {
   width: 100px;
   height: 100px;
   background-color: green;
+  /* Adjust for subtracted 100px in class=spacer */
+  margin-top: 100px;
 }
 </style>
 <div class="spacer"></div>
@@ -31,21 +38,26 @@
   assert_true(!!target, "Target exists");
   function createObserver() {
     new IntersectionObserver(function(changes) {
-      entries = entries.concat(changes)
+      entries = entries.concat(changes);
+      window.testRunner.DoNonMeasuredLayout();
     }).observe(target);
   }
   createObserver();
   runTestCycle(step0, "First rAF");
 }, "IntersectionObserver that is unreachable in js should still generate notifications.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 300;
+  document.getElementById("target").style.marginTop = "-200px";
+  //document.scrollingElement.scrollTop = 300;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
   assert_equals(entries.length, 1, "One notification.");
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 0;
+  document.getElementById("target").style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
   assert_equals(entries.length, 2, "Two notifications.");
 }
 </script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
index 63e9f86..957ad77 100644
--- a/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
@@ -5,18 +5,25 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
 }
 #target {
   width: 100px;
   height: 100px;
   background-color: green;
+  /* Adjust for subtracted 100px in class=spacer */
+  margin-top: 100px;
 }
 </style>
 
@@ -25,8 +32,8 @@
 <div class="spacer"></div>
 
 <script>
-var vw = document.documentElement.clientWidth;
-var vh = document.documentElement.clientHeight;
+var vw = window.innerWidth;
+var vh = window.innerHeight;
 
 var entries = [];
 var target;
@@ -35,28 +42,34 @@
   target = document.getElementById("target");
   assert_true(!!target, "target exists");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   });
   observer.observe(target);
   entries = entries.concat(observer.takeRecords());
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF.");
 }, "IntersectionObserver in a single document using the implicit root.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 300;
+  target.style.marginTop = "-200px";
+  //document.scrollingElement.scrollTop = 300;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
   checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 100;
+  target.style.marginTop = "0px";
+  //document.scrollingElement.scrollTop = 100;
   runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
   checkLastEntry(entries, 1, [8, 108, vh - 192, vh - 92, 8, 108, vh - 192, vh - 92, 0, vw, 0, vh, true]);
 }
 
 function step2() {
-  document.scrollingElement.scrollTop = 0;
+  target.style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
   checkLastEntry(entries, 2, [8, 108, vh + 8, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 </script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
index 20bd11d..ddf6fec 100644
--- a/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
@@ -5,18 +5,25 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
 }
 #target {
   width: 0px;
   height: 0px;
   background-color: green;
+  /* Adjust for subtracted 100px in class=spacer */
+  margin-top: 100px;
 }
 </style>
 
@@ -25,8 +32,8 @@
 <div class="spacer"></div>
 
 <script>
-var vw = document.documentElement.clientWidth;
-var vh = document.documentElement.clientHeight;
+var vw = window.innerWidth;
+var vh = window.innerHeight;
 
 var entries = [];
 var target;
@@ -35,28 +42,34 @@
   target = document.getElementById("target");
   assert_true(!!target, "Target exists");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   });
   observer.observe(target);
   entries = entries.concat(observer.takeRecords());
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF");
 }, "Observing a zero-area target.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 300;
+  target.style.marginTop = "-200px";
+  //document.scrollingElement.scrollTop = 300;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
   checkLastEntry(entries, 0, [8, 8, vh + 108, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 100;
+  target.style.marginTop = "0px";
+  //document.scrollingElement.scrollTop = 100;
   runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
   checkLastEntry(entries, 1, [8, 8, vh - 192, vh - 192, 8, 8, vh - 192, vh - 192, 0, vw, 0, vh, true]);
 }
 
 function step2() {
-  document.scrollingElement.scrollTop = 0;
+  target.style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
   checkLastEntry(entries, 2, [8, 8, vh + 8, vh + 8, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 </script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/text-target.html b/src/third_party/web_platform_tests/intersection-observer/text-target.html
index 1abe535..2317a66 100644
--- a/src/third_party/web_platform_tests/intersection-observer/text-target.html
+++ b/src/third_party/web_platform_tests/intersection-observer/text-target.html
@@ -5,23 +5,33 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
   left: 200px;
 }
 .spacer {
-  height: calc(100vh + 100px);
+  /* Cobalt does not support calc */
+  height: 100vh;
+}
+#target {
+  /* Adjust for subtracted 100px in class=spacer */
+  margin-top: 100px;
 }
 </style>
 
 <div class="spacer"></div>
-<br id="target">
+<!--br gives incorrect bounding rect in Cobalt so we use p instead-->
+<p id="target">
 <div class="spacer"></div>
 
 <script>
-var vw = document.documentElement.clientWidth;
-var vh = document.documentElement.clientHeight;
+var vw = window.innerWidth;
+var vh = window.innerHeight;
 
 var entries = [];
 var target;
@@ -34,16 +44,20 @@
   th = target_rect.height;
   assert_true(!!target, "target exists");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   });
   observer.observe(target);
   entries = entries.concat(observer.takeRecords());
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF.");
-}, "IntersectionObserver observing a br element.");
+}, "IntersectionObserver observing a p element.");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
-  document.scrollingElement.scrollTop = 300;
+  target.style.marginTop = "-200px";
+  //document.scrollingElement.scrollTop = 300;
   runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
   // The numbers in brackets are target client rect; intersection rect;
   // and root bounds.
@@ -51,13 +65,15 @@
 }
 
 function step1() {
-  document.scrollingElement.scrollTop = 100;
+  target.style.marginTop = "0px";
+  //document.scrollingElement.scrollTop = 100;
   runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
   checkLastEntry(entries, 1, [8, 8 + tw, vh - 192, vh - 192 + th, 8, 8 + tw, vh - 192, vh - 192 + th, 0, vw, 0, vh, true]);
 }
 
 function step2() {
-  document.scrollingElement.scrollTop = 0;
+  target.style.marginTop = "100px";
+  //document.scrollingElement.scrollTop = 0;
   checkLastEntry(entries, 2, [8, 8 + tw, vh + 8, vh + 8 + th, 0, 0, 0, 0, 0, vw, 0, vh, false]);
 }
 </script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
index 5431750..be8aa0a 100644
--- a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
+++ b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
@@ -5,6 +5,10 @@
 <script src="./resources/intersection-observer-test-utils.js"></script>
 
 <style>
+/* Cobalt does not implement HTML5 spec for body margin. */
+body {
+  margin: 8px;
+}
 pre, #log {
   position: absolute;
   top: 0;
@@ -25,13 +29,16 @@
   var target = document.getElementById('target');
   assert_true(!!target, "target exists");
   var observer = new IntersectionObserver(function(changes) {
-    entries = entries.concat(changes)
+    entries = entries.concat(changes);
+    window.testRunner.DoNonMeasuredLayout();
   });
   observer.observe(target);
   entries = entries.concat(observer.takeRecords());
   assert_equals(entries.length, 0, "No initial notifications.");
   runTestCycle(step0, "First rAF should generate a notification.");
 }, "Ensure that a zero-area target intersecting root generates a notification with intersectionRatio == 1");
+window.testRunner.DoNonMeasuredLayout();
+window.testRunner.DoNonMeasuredLayout();
 
 function step0() {
   assert_equals(entries.length, 1, "One notification.");
diff --git a/src/third_party/zlib/zlib.gyp b/src/third_party/zlib/zlib.gyp
index aee6d67..1156168 100644
--- a/src/third_party/zlib/zlib.gyp
+++ b/src/third_party/zlib/zlib.gyp
@@ -520,7 +520,6 @@
         'minizip',
         'zip',
         'zlib',
-        '<(DEPTH)/base/base.gyp:base',
         '<(DEPTH)/base/base.gyp:test_support_base',
         '<(DEPTH)/starboard/common/common.gyp:common',
         '<(DEPTH)/testing/gmock.gyp:gmock',
diff --git a/src/v8/src/base/platform/platform-starboard.cc b/src/v8/src/base/platform/platform-starboard.cc
index 3cd69ec..27bfd38 100644
--- a/src/v8/src/base/platform/platform-starboard.cc
+++ b/src/v8/src/base/platform/platform-starboard.cc
@@ -17,9 +17,8 @@
 #include "src/base/platform/platform.h"
 #include "src/base/platform/time.h"
 #include "src/base/utils/random-number-generator.h"
-
 #include "src/base/timezone-cache.h"
-
+#include "starboard/client_porting/eztime/eztime.h"
 #include "starboard/common/condition_variable.h"
 #include "starboard/common/log.h"
 #include "starboard/common/string.h"
@@ -468,7 +467,6 @@
 
 class StarboardTimezoneCache : public TimezoneCache {
  public:
-  double DaylightSavingsOffset(double time_ms) override { return 0.0; }
   void Clear(TimeZoneDetection time_zone_detection) override {}
   ~StarboardTimezoneCache() override {}
 
@@ -482,7 +480,18 @@
     return SbTimeZoneGetName();
   }
   double LocalTimeOffset(double time_ms, bool is_utc) override {
-    return SbTimeZoneGetCurrent() * 60000.0;
+    // SbTimeZOneGetCurrent returns an offset west of Greenwich, which has the
+    // opposite sign V8 expects.
+    // The starboard function returns offset in minutes. We convert to return
+    // value in milliseconds.
+    return SbTimeZoneGetCurrent() * 60.0 * msPerSecond * (-1);
+  }
+  double DaylightSavingsOffset(double time_ms) override {
+    EzTimeValue value = EzTimeValueFromSbTime(SbTimeGetNow());
+    EzTimeExploded ez_exploded;
+    bool result = EzTimeValueExplode(&value, kEzTimeZoneLocal, &ez_exploded,
+                                     NULL);
+    return ez_exploded.tm_isdst > 0 ? 3600 * msPerSecond : 0;
   }
 
   ~StarboardDefaultTimezoneCache() override {}