Import Cobalt 20.lts.3.244012
diff --git a/src/cobalt/CHANGELOG.md b/src/cobalt/CHANGELOG.md
index 70b84b5..aa05c04 100644
--- a/src/cobalt/CHANGELOG.md
+++ b/src/cobalt/CHANGELOG.md
@@ -180,6 +180,12 @@
      minimum framerate causing Cobalt to rerender the display even if nothing has
      changed after the specified interval.
 
+### Version 20.lts.3
+ - **Improvements and Bug Fixes**
+
+   - Fix a bug where deep links that are fired before the WebModule is loaded
+     were ignored. Now Cobalt stores the last deep link fired before WebModule
+     is loaded & handles the deep link once the WebModule is loaded.
 
 ## Version 19
  - **Add support for V8 JavaScript Engine**
diff --git a/src/cobalt/build/build.id b/src/cobalt/build/build.id
index 4bc47a5..e912fc3 100644
--- a/src/cobalt/build/build.id
+++ b/src/cobalt/build/build.id
@@ -1 +1 @@
-238772
\ No newline at end of file
+244012
\ No newline at end of file
diff --git a/src/cobalt/build/cobalt_configuration.gypi b/src/cobalt/build/cobalt_configuration.gypi
index 9feeca1..05ff945 100644
--- a/src/cobalt/build/cobalt_configuration.gypi
+++ b/src/cobalt/build/cobalt_configuration.gypi
@@ -230,7 +230,7 @@
     # Set to "true" to enable v8 snapshot generation at Cobalt build time.
     'cobalt_v8_buildtime_snapshot%': '<(cobalt_v8_buildtime_snapshot)',
 
-    'cobalt_enable_quic': 1,
+    'cobalt_enable_quic%': 1,
 
     # Cache parameters
 
diff --git a/src/cobalt/tools/automated_testing/cobalt_runner.py b/src/cobalt/tools/automated_testing/cobalt_runner.py
index c710a3a..0e6f30e 100644
--- a/src/cobalt/tools/automated_testing/cobalt_runner.py
+++ b/src/cobalt/tools/automated_testing/cobalt_runner.py
@@ -11,6 +11,7 @@
 import thread
 import threading
 import time
+import traceback
 
 import _env  # pylint: disable=unused-import
 from cobalt.tools.automated_testing import c_val_names
@@ -251,9 +252,18 @@
 
   def _KillLauncher(self):
     """Kills the launcher and its attached Cobalt instance."""
-    self.ExecuteJavaScript('window.close();')
+    wait_for_runner_thread = True
+    if self.CanExecuteJavaScript():
+      try:
+        self.ExecuteJavaScript('window.close();')
+      except Exception:
+        wait_for_runner_thread = False
+        sys.stderr.write(
+            '***An exception was raised while trying to close the app:')
+        traceback.print_exc(file=sys.stderr)
 
-    self.runner_thread.join(COBALT_EXIT_TIMEOUT_SECONDS)
+    if wait_for_runner_thread:
+      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'
@@ -309,6 +319,9 @@
         thread.interrupt_main()
     return 0
 
+  def CanExecuteJavaScript(self):
+    return self.webdriver is not None
+
   def ExecuteJavaScript(self, js_code):
     return self.webdriver.execute_script(js_code)
 
@@ -496,7 +509,7 @@
   device_params.config = args.config
   device_params.device_id = args.device_id
   device_params.out_directory = args.out_directory
-  if args.target_params == None:
+  if args.target_params is None:
     device_params.target_params = []
   else:
     device_params.target_params = [args.target_params]
diff --git a/src/starboard/__init__.py b/src/starboard/__init__.py
index b2efd6d..1710e45 100644
--- a/src/starboard/__init__.py
+++ b/src/starboard/__init__.py
@@ -12,6 +12,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
 """Initialization for the starboard package."""
 
 # Provide a holder for the config package to insert an adapter for platforms
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 63d27d8..1c597b2 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
@@ -101,6 +101,7 @@
     public static final String VIDEO_H265 = "video/hevc";
     public static final String VIDEO_VP8 = "video/x-vnd.on2.vp8";
     public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
+    public static final String VIDEO_AV1 = "video/av01";
   }
 
   private BitrateAdjustmentTypes mBitrateAdjustmentType = BitrateAdjustmentTypes.NO_ADJUSTMENT;
@@ -858,6 +859,7 @@
         break;
       case MimeTypes.VIDEO_H265:
       case MimeTypes.VIDEO_VP9:
+      case MimeTypes.VIDEO_AV1:
         maxPixels = maxWidth * maxHeight;
         minCompressionRatio = 4;
         break;
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
index ee85466..24fe96f 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java
@@ -110,10 +110,12 @@
       this.mErrorMessage = errorMessage;
     }
 
+    @UsedByNative
     public boolean isSuccess() {
       return mIsSuccess;
     }
 
+    @UsedByNative
     public String getErrorMessage() {
       return mErrorMessage;
     }
diff --git a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
index 315e679..c471b0c 100644
--- a/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
+++ b/src/starboard/android/apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java
@@ -16,8 +16,8 @@
 
 import static dev.cobalt.util.Log.TAG;
 
+import android.os.FileObserver;
 import android.support.annotation.Nullable;
-import android.text.TextUtils;
 import com.google.protobuf.InvalidProtocolBufferException;
 import dev.cobalt.storage.StorageProto.Storage;
 import dev.cobalt.util.Log;
@@ -27,9 +27,13 @@
 import java.io.IOException;
 import java.util.Arrays;
 
-/** A class to load Cobalt storage from the file system. */
+/**
+ * A class to load Cobalt storage from the file system.
+ */
 public class CobaltStorageLoader {
+
   private final File filesDir;
+  private final StorageFileObserver fileObserver;
 
   private static final String STORAGE_NAME_PREFIX = ".starboard";
   private static final String STORAGE_NAME_SUFFIX = ".storage";
@@ -45,6 +49,7 @@
       throw new IllegalArgumentException("A valid filesDir object is required");
     }
     this.filesDir = filesDir;
+    this.fileObserver = StorageFileObserver.create(filesDir);
   }
 
   /**
@@ -57,14 +62,20 @@
     if (fileName == null) {
       return Storage.getDefaultInstance();
     }
-    byte[] storageBlob = getStorageBlob(fileName);
+    Storage storage = loadStorageFile(new File(filesDir, fileName));
+    return (storage != null) ? storage : Storage.getDefaultInstance();
+  }
+
+  @Nullable
+  private static Storage loadStorageFile(File file) {
+    byte[] storageBlob = getStorageBlob(file);
     if (storageBlob == null) {
       Log.e(TAG, "Failed to get storage blob");
-      return Storage.getDefaultInstance();
+      return null;
     }
     if (!validateStorageBlob(storageBlob)) {
       Log.e(TAG, "Invalid storage blob");
-      return Storage.getDefaultInstance();
+      return null;
     }
     storageBlob = Arrays.copyOfRange(storageBlob, STORAGE_HEADER.length(), storageBlob.length);
     try {
@@ -72,10 +83,10 @@
     } catch (InvalidProtocolBufferException e) {
       Log.e(TAG, "Failed parsing the blob", e);
     }
-    return Storage.getDefaultInstance();
+    return null;
   }
 
-  private boolean validateStorageBlob(byte[] storageBlob) {
+  private static boolean validateStorageBlob(byte[] storageBlob) {
     if (storageBlob == null || storageBlob.length < STORAGE_HEADER.length()) {
       return false;
     }
@@ -84,12 +95,8 @@
   }
 
   @Nullable
-  private byte[] getStorageBlob(String fileName) {
-    if (TextUtils.isEmpty(fileName)) {
-      Log.e(TAG, "Invalid empty file name");
-      return null;
-    }
-    try (FileInputStream in = new FileInputStream(new File(filesDir, fileName));
+  private static byte[] getStorageBlob(File file) {
+    try (FileInputStream in = new FileInputStream(file);
         ByteArrayOutputStream out = new ByteArrayOutputStream()) {
       byte[] buffer = new byte[8192];
       int len;
@@ -111,7 +118,7 @@
       return null;
     }
     for (String fileName : fileNames) {
-      if (fileName.startsWith(STORAGE_NAME_PREFIX) && fileName.endsWith(STORAGE_NAME_SUFFIX)) {
+      if (isStorageFile(fileName)) {
         File storageFile = new File(filesDir, fileName);
         if (storageFile.length() > 0) {
           return fileName;
@@ -121,4 +128,73 @@
     Log.w(TAG, "Failed to find storage file name");
     return null;
   }
+
+  private static boolean isStorageFile(String fileName) {
+    return fileName != null
+        && fileName.startsWith(STORAGE_NAME_PREFIX)
+        && fileName.endsWith(STORAGE_NAME_SUFFIX);
+  }
+
+  /**
+   * Set the observer to be notified when storage changes.
+   *
+   * @param storageObserver will be called when storage changes.
+   */
+  public void setObserver(@Nullable CobaltStorageObserver storageObserver) {
+    fileObserver.setStorageObserver(storageObserver);
+  }
+
+  /**
+   * Observer that is called when Cobalt storage changes.
+   */
+  public interface CobaltStorageObserver {
+
+    /**
+     * Called when Cobalt storage changes.
+     *
+     * <p>This method is invoked on a special thread. It runs independently of any threads, so take
+     * care to use appropriate synchronization! Consider using Handler#post(Runnable) to shift event
+     * handling work to the main thread to avoid concurrency problems.
+     *
+     * @param snapshot the Cobalt storage as a proto message.
+     */
+    void onCobaltStorageChanged(Storage snapshot);
+  }
+
+  private static class StorageFileObserver extends FileObserver {
+
+    private final File filesDir;
+    private CobaltStorageObserver storageObserver = null;
+
+    static StorageFileObserver create(File filesDir) {
+      return new StorageFileObserver(filesDir.getAbsolutePath());
+    }
+
+    StorageFileObserver(String filesDirPath) {
+      super(filesDirPath, MOVED_TO);
+      this.filesDir = new File(filesDirPath);
+    }
+
+    void setStorageObserver(@Nullable CobaltStorageObserver storageObserver) {
+      if (this.storageObserver != null) {
+        stopWatching();
+      }
+      this.storageObserver = storageObserver;
+      if (this.storageObserver != null) {
+        startWatching();
+      }
+    }
+
+    @Override
+    public void onEvent(int event, @Nullable String fileName) {
+      // Expect a MOVED_TO event since SbStorageWrite() creates a temp file then moves it in place.
+      if (event != MOVED_TO || storageObserver == null || !isStorageFile(fileName)) {
+        return;
+      }
+      Storage storage = loadStorageFile(new File(filesDir, fileName));
+      if (storage != null) {
+        storageObserver.onCobaltStorageChanged(storage);
+      }
+    }
+  }
 }
diff --git a/src/starboard/android/shared/cobalt/configuration.gypi b/src/starboard/android/shared/cobalt/configuration.gypi
index 35647ce..27b99cd 100644
--- a/src/starboard/android/shared/cobalt/configuration.gypi
+++ b/src/starboard/android/shared/cobalt/configuration.gypi
@@ -22,6 +22,8 @@
     'enable_account_manager': 1,
     'enable_map_to_mesh': 1,
 
+    'cobalt_enable_quic': 0,
+
     # Some Android devices do not advertise their support for
     # EGL_SWAP_BEHAVIOR_PRESERVED_BIT properly, so, we play it safe and disable
     # relying on it for Android.
diff --git a/src/starboard/android/shared/media_common.h b/src/starboard/android/shared/media_common.h
index 9e47335..2402f6a 100644
--- a/src/starboard/android/shared/media_common.h
+++ b/src/starboard/android/shared/media_common.h
@@ -59,6 +59,8 @@
     return "video/x-vnd.on2.vp9";
   } else if (video_codec == kSbMediaVideoCodecH264) {
     return "video/avc";
+  } else if (video_codec == kSbMediaVideoCodecAv1) {
+    return "video/av01";
   }
   return NULL;
 }
diff --git a/src/starboard/android/shared/player_create.cc b/src/starboard/android/shared/player_create.cc
index ffa2260..7edd8d6 100644
--- a/src/starboard/android/shared/player_create.cc
+++ b/src/starboard/android/shared/player_create.cc
@@ -76,7 +76,8 @@
 
   if (video_codec != kSbMediaVideoCodecNone &&
       video_codec != kSbMediaVideoCodecH264 &&
-      video_codec != kSbMediaVideoCodecVp9) {
+      video_codec != kSbMediaVideoCodecVp9 &&
+      video_codec != kSbMediaVideoCodecAv1) {
     SB_LOG(ERROR) << "Unsupported video codec " << video_codec;
     return kSbPlayerInvalid;
   }
diff --git a/src/starboard/tools/port_symlink.py b/src/starboard/tools/port_symlink.py
new file mode 100644
index 0000000..a4a8762
--- /dev/null
+++ b/src/starboard/tools/port_symlink.py
@@ -0,0 +1,6 @@
+"""A redirect for new build code to find the old port_symlink.py location."""
+
+import os
+
+execfile(os.path.join(os.path.dirname(__file__), os.pardir, 'build',
+                      'port_symlink.py'))