Merge "Update all macrobenchmark targets to track TOT profileinstaller" into androidx-main
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 740db84..48236d48 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -36,7 +36,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
 }
 
 androidx {
diff --git a/busytown/androidx-native-mac-host-tests.sh b/busytown/androidx-native-mac-host-tests.sh
index 622d057..b39a384 100755
--- a/busytown/androidx-native-mac-host-tests.sh
+++ b/busytown/androidx-native-mac-host-tests.sh
@@ -1,6 +1,5 @@
 #!/bin/bash
 set -e
-cd "$(dirname $0)"
 
 export ANDROIDX_PROJECTS=KMP
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
index 5887e59b..a5d878f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
@@ -60,6 +60,28 @@
     }
 
     /**
+     * Retrieves all {@link Quirk}s of the same or inherited type as the given type.
+     *
+     * <p>Unlike {@link #get(Class)}, a quirk can only be retrieved by the exact class. If a
+     * superclass or superinterface is provided, all the inherited classes will be returned.
+     *
+     * @param quirkClass The super type of quirk to retrieve.
+     * @return A {@link Quirk} list of the provided type. An empty list is returned if it isn't
+     * found.
+     */
+    @SuppressWarnings("unchecked")
+    @NonNull
+    public <T extends Quirk> List<T> getAll(@NonNull Class<T> quirkClass) {
+        List<T> list = new ArrayList<>();
+        for (Quirk quirk : mQuirks) {
+            if (quirkClass.isAssignableFrom(quirk.getClass())) {
+                list.add((T) quirk);
+            }
+        }
+        return list;
+    }
+
+    /**
      * Returns whether this collection of quirks contains a quirk with the provided type.
      *
      * <p>This checks whether the provided quirk type is the exact class, a superclass, or a
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
index 9b780d9..8cb5c42 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
@@ -56,6 +56,34 @@
     }
 
     @Test
+    public void getAllReturnsExactAndInheritedQuirks() {
+        SuperQuirk superQuirk = new SuperQuirk();
+        SubQuirk subQuirk = new SubQuirk();
+
+        List<Quirk> allQuirks = new ArrayList<>();
+        allQuirks.add(superQuirk);
+        allQuirks.add(subQuirk);
+
+        Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.getAll(SubQuirk.class)).containsExactly(subQuirk);
+        assertThat(quirks.getAll(SuperQuirk.class)).containsExactly(superQuirk, subQuirk);
+    }
+
+    @Test
+    public void getAllReturnsImplementedQuirks() {
+        SubIQuirk subIQuirk = new SubIQuirk();
+
+        List<Quirk> allQuirks = new ArrayList<>();
+        allQuirks.add(subIQuirk);
+
+        Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.getAll(SubIQuirk.class)).containsExactly(subIQuirk);
+        assertThat(quirks.getAll(ISuperQuirk.class)).containsExactly(subIQuirk);
+    }
+
+    @Test
     public void containsReturnsTrueForExistentQuirk() {
         final Quirk1 quirk1 = new Quirk1();
 
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
index 6246d93..dfd3af6 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -30,15 +30,11 @@
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.video.internal.compat.quirk.ExcludeStretchedVideoQualityQuirk;
-import androidx.camera.video.internal.compat.quirk.ReportedVideoQualityNotSupportedQuirk;
-import androidx.camera.video.internal.compat.quirk.VideoEncoderCrashQuirk;
 import androidx.camera.video.internal.compat.quirk.VideoQualityQuirk;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Deque;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -87,7 +83,7 @@
 
             // Get CamcorderProfile
             if (!camcorderProfileProvider.hasProfile(qualityValue) || !isDeviceValidQuality(
-                    quality)) {
+                    cameraInfoInternal, quality)) {
                 continue;
             }
             CamcorderProfileProxy profile =
@@ -230,17 +226,11 @@
                 "Unknown quality: " + quality);
     }
 
-    private boolean isDeviceValidQuality(@NonNull Quality quality) {
-        List<Class<? extends VideoQualityQuirk>> quirkList = Arrays.asList(
-                ExcludeStretchedVideoQualityQuirk.class,
-                ReportedVideoQualityNotSupportedQuirk.class,
-                VideoEncoderCrashQuirk.class
-        );
-
-        for (Class<? extends VideoQualityQuirk> quirkClass : quirkList) {
-            VideoQualityQuirk quirk = DeviceQuirks.get(quirkClass);
-
-            if (quirk != null && quirk.isProblematicVideoQuality(quality)) {
+    private boolean isDeviceValidQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull Quality quality) {
+        for (VideoQualityQuirk quirk : DeviceQuirks.getAll(VideoQualityQuirk.class)) {
+            if (quirk != null && quirk.isProblematicVideoQuality(cameraInfo, quality)
+                    && !quirk.workaroundBySurfaceProcessing()) {
                 return false;
             }
         }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index c250892..f29a28f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -105,6 +105,7 @@
 import androidx.camera.video.internal.compat.quirk.ImageCaptureFailedWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.compat.quirk.PreviewDelayWhenVideoCaptureIsBoundQuirk;
 import androidx.camera.video.internal.compat.quirk.PreviewStretchWhenVideoCaptureIsBoundQuirk;
+import androidx.camera.video.internal.compat.quirk.VideoQualityQuirk;
 import androidx.camera.video.internal.config.MimeInfo;
 import androidx.camera.video.internal.encoder.InvalidConfigException;
 import androidx.camera.video.internal.encoder.VideoEncoderConfig;
@@ -152,12 +153,22 @@
     private static final String SURFACE_UPDATE_KEY =
             "androidx.camera.video.VideoCapture.streamUpdate";
     private static final Defaults DEFAULT_CONFIG = new Defaults();
-    private static final boolean HAS_PREVIEW_STRETCH_QUIRK =
-            DeviceQuirks.get(PreviewStretchWhenVideoCaptureIsBoundQuirk.class) != null;
-    private static final boolean HAS_PREVIEW_DELAY_QUIRK =
-            DeviceQuirks.get(PreviewDelayWhenVideoCaptureIsBoundQuirk.class) != null;
-    private static final boolean HAS_IMAGE_CAPTURE_QUIRK =
-            DeviceQuirks.get(ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.class) != null;
+    private static final boolean ENABLE_SURFACE_PROCESSING_BY_QUIRK;
+    private static final boolean USE_TEMPLATE_PREVIEW_BY_QUIRK;
+    static {
+        boolean hasPreviewStretchQuirk =
+                DeviceQuirks.get(PreviewStretchWhenVideoCaptureIsBoundQuirk.class) != null;
+        boolean hasPreviewDelayQuirk =
+                DeviceQuirks.get(PreviewDelayWhenVideoCaptureIsBoundQuirk.class) != null;
+        boolean hasImageCaptureFailedQuirk =
+                DeviceQuirks.get(ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.class) != null;
+        boolean hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing =
+                hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing();
+        USE_TEMPLATE_PREVIEW_BY_QUIRK =
+                hasPreviewStretchQuirk || hasPreviewDelayQuirk || hasImageCaptureFailedQuirk;
+        ENABLE_SURFACE_PROCESSING_BY_QUIRK = hasPreviewDelayQuirk || hasImageCaptureFailedQuirk
+                || hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing;
+    }
 
     @SuppressWarnings("WeakerAccess") // Synthetic access
     DeferrableSurface mDeferrableSurface;
@@ -535,7 +546,7 @@
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
         sessionConfigBuilder.addErrorListener(
                 (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
-        if (HAS_PREVIEW_STRETCH_QUIRK || HAS_PREVIEW_DELAY_QUIRK || HAS_IMAGE_CAPTURE_QUIRK) {
+        if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
             sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
         }
 
@@ -698,8 +709,8 @@
 
     @Nullable
     private SurfaceProcessorNode createNodeIfNeeded() {
-        if (mSurfaceProcessor != null || HAS_PREVIEW_DELAY_QUIRK || HAS_IMAGE_CAPTURE_QUIRK) {
-            Logger.d(TAG, "SurfaceEffect is enabled.");
+        if (mSurfaceProcessor != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK) {
+            Logger.d(TAG, "Surface processing is enabled.");
             return new SurfaceProcessorNode(requireNonNull(getCamera()),
                     APPLY_CROP_ROTATE_AND_MIRRORING,
                     mSurfaceProcessor != null ? mSurfaceProcessor : new DefaultSurfaceProcessor());
@@ -1085,6 +1096,16 @@
         return ret;
     }
 
+    private static boolean hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing() {
+        List<VideoQualityQuirk> quirks = DeviceQuirks.getAll(VideoQualityQuirk.class);
+        for (VideoQualityQuirk quirk : quirks) {
+            if (quirk.workaroundBySurfaceProcessing()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static int getArea(@NonNull Size size) {
         return size.getWidth() * size.getHeight();
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
index 02a2ce6..10393b1 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -22,6 +22,8 @@
 import androidx.camera.core.impl.Quirk;
 import androidx.camera.core.impl.Quirks;
 
+import java.util.List;
+
 /**
  * Provider of video capture related quirks, which are used for device or API level specific
  * workarounds.
@@ -58,4 +60,16 @@
     public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
         return QUIRKS.get(quirkClass);
     }
+
+    /**
+     * Retrieves all video {@link Quirk} instances that are or inherit the given type.
+     *
+     * @param quirkClass The super type of video quirk to retrieve.
+     * @return A video {@link Quirk} list of the provided type. An empty list is returned if it
+     * isn't found.
+     */
+    @NonNull
+    public static <T extends Quirk> List<T> getAll(@NonNull final Class<T> quirkClass) {
+        return QUIRKS.getAll(quirkClass);
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
index 8e7d374..63cea22 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeStretchedVideoQualityQuirk.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.video.Quality;
 
 /**
@@ -52,7 +53,8 @@
 
     /** Checks if the given Quality type is a problematic quality. */
     @Override
-    public boolean isProblematicVideoQuality(@NonNull Quality quality) {
+    public boolean isProblematicVideoQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull Quality quality) {
         if (isSamsungJ4()) {
             return quality == Quality.FHD || quality == Quality.UHD;
         }
@@ -61,4 +63,7 @@
         }
         return false;
     }
+
+    // TODO: determine if the issue can be workaround by effect pipeline and if we want to do this,
+    //  then override workaroundBySurfaceProcessing().
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ReportedVideoQualityNotSupportedQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ReportedVideoQualityNotSupportedQuirk.java
index 04d1ecd..acf2bd5 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ReportedVideoQualityNotSupportedQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ReportedVideoQualityNotSupportedQuirk.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.video.internal.compat.quirk;
 
+import static androidx.camera.core.CameraSelector.LENS_FACING_FRONT;
+
 import android.media.CamcorderProfile;
 import android.media.MediaCodec;
 import android.media.MediaRecorder.VideoEncoder;
@@ -24,31 +26,46 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.CamcorderProfileProvider;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.video.Quality;
 
+import java.util.Arrays;
+import java.util.Locale;
+
 /**
  * Quirk where qualities reported as available by {@link CamcorderProfileProvider#hasProfile(int)}
  * does not work on the device, and should not be used.
  *
  * <p>QuirkSummary
- *      Bug Id:      202080832, 242526718
- *      Description: On devices exhibiting this quirk, {@link CamcorderProfile} indicates it
- *                   can support resolutions for a specific video encoder (e.g., 3840x2160 for
- *                   {@link VideoEncoder#H264} on Huawei Mate 20), and it can create the video
- *                   encoder by the corresponding format. However, the camera is unable to produce
- *                   video frames when configured with a {@link MediaCodec} surface at the
- *                   specified resolution. On these devices, the capture session is opened and
- *                   configured, but an error occurs in the HAL. See b/202080832#comment8
- *                   for details of this error. See b/242526718#comment2. On Vivo Y91i,
- *                   {@link CamcorderProfile} indicates AVC encoder can support resolutions
- *                   1920x1080 and 1280x720. However, the 1920x1080 and 1280x720 options cannot be
- *                   configured properly. It only supports 640x480.
- *      Device(s):   Huawei Mate 20, Huawei Mate 20 Pro, Vivo Y91i
+ *      Bug Id: 202080832, 242526718, 250807400
+ *      Description:
+ *                   <ul>
+ *                       <li>See b/202080832#comment8. On devices exhibiting this quirk,
+ *                       {@link CamcorderProfile} indicates it can support resolutions for a
+ *                       specific video encoder (e.g., 3840x2160 for {@link VideoEncoder#H264} on
+ *                       Huawei Mate 20), and it can create the video encoder by the
+ *                       corresponding format. However, the camera is unable to produce video
+ *                       frames when configured with a {@link MediaCodec} surface at the
+ *                       specified resolution. On these devices, the capture session is opened
+ *                       and configured, but an error occurs in the HAL.  for details of this
+ *                       error.</li>
+ *                  </ul>
+ *                  <ul>
+ *                      <li>See b/242526718#comment2. On Vivo Y91i, {@link CamcorderProfile}
+ *                      indicates AVC encoder can support resolutions 1920x1080 and 1280x720.
+ *                      However, the 1920x1080 and 1280x720 options cannot be configured properly.
+ *                      It only supports 640x480.</li>
+ *                  </ul>
+ *                  <ul>
+ *                      <li>See b/250807400. On Huawei P40 Lite, it fails to record video on
+ *                      front camera and FHD/HD quality.</li>
+ *                  </ul>
+ *      Device(s):   Huawei Mate 20, Huawei Mate 20 Pro, Vivo Y91i, Huawei P40 Lite
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ReportedVideoQualityNotSupportedQuirk implements VideoQualityQuirk {
     static boolean load() {
-        return isHuaweiMate20() || isHuaweiMate20Pro() || isVivoY91i();
+        return isHuaweiMate20() || isHuaweiMate20Pro() || isVivoY91i() || isHuaweiP40Lite();
     }
 
     private static boolean isHuaweiMate20() {
@@ -63,16 +80,32 @@
         return "Vivo".equalsIgnoreCase(Build.BRAND) && "vivo 1820".equalsIgnoreCase(Build.MODEL);
     }
 
+    private static boolean isHuaweiP40Lite() {
+        return "Huawei".equalsIgnoreCase(Build.MANUFACTURER)
+                && Arrays.asList("JNY-L21A", "JNY-L01A", "JNY-L21B", "JNY-L22A", "JNY-L02A",
+                "JNY-L22B", "JNY-LX1").contains(Build.MODEL.toUpperCase(Locale.US));
+    }
+
     /** Checks if the given mime type is a problematic quality. */
     @Override
-    public boolean isProblematicVideoQuality(@NonNull Quality quality) {
+    public boolean isProblematicVideoQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull Quality quality) {
         if (isHuaweiMate20() || isHuaweiMate20Pro()) {
             return quality == Quality.UHD;
         } else if (isVivoY91i()) {
             // On Y91i, the HD and FHD resolution is problematic with the front camera. The back
             // camera only supports SD resolution.
             return quality == Quality.HD || quality == Quality.FHD;
+        } else if (isHuaweiP40Lite()) {
+            return cameraInfo.getLensFacing() == LENS_FACING_FRONT
+                    && (quality == Quality.FHD || quality == Quality.HD);
         }
         return false;
     }
+
+    @Override
+    public boolean workaroundBySurfaceProcessing() {
+        // VivoY91i can't be workaround.
+        return isHuaweiMate20() || isHuaweiMate20Pro() || isHuaweiP40Lite();
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoEncoderCrashQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoEncoderCrashQuirk.java
index 14a9cb2..7d4da23 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoEncoderCrashQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoEncoderCrashQuirk.java
@@ -16,10 +16,13 @@
 
 package androidx.camera.video.internal.compat.quirk;
 
+import static androidx.camera.core.CameraSelector.LENS_FACING_FRONT;
+
 import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.video.Quality;
 
 /**
@@ -44,10 +47,17 @@
 
     /** Checks if the given Quality type is a problematic quality. */
     @Override
-    public boolean isProblematicVideoQuality(@NonNull Quality quality) {
+    public boolean isProblematicVideoQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull Quality quality) {
         if (isPositivoTwist2Pro()) {
-            return quality == Quality.SD;
+            return cameraInfo.getLensFacing() == LENS_FACING_FRONT && quality == Quality.SD;
         }
         return false;
     }
+
+    @Override
+    public boolean workaroundBySurfaceProcessing() {
+        // Failed to record video for front camera + SD quality. See b/218841498 comment#5.
+        return false;
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoQualityQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoQualityQuirk.java
index e13e5e8..e49f34f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoQualityQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/VideoQualityQuirk.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.Quirk;
 import androidx.camera.video.Quality;
 
@@ -31,5 +32,13 @@
 public interface VideoQualityQuirk extends Quirk {
 
     /** Checks if the given Quality type is a problematic quality. */
-    boolean isProblematicVideoQuality(@NonNull Quality quality);
+    boolean isProblematicVideoQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull Quality quality);
+
+    /**
+     * Returns true if the problem can be workaround by surface processing and we want to enable it.
+     */
+    default boolean workaroundBySurfaceProcessing() {
+        return false;
+    }
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
index 9f668ea..30a53cc 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -20,6 +20,7 @@
 import androidx.annotation.Nullable;
 import androidx.camera.core.impl.Quirk;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -56,4 +57,24 @@
         }
         return null;
     }
+
+    /**
+     * Retrieves all device {@link Quirk} instances that are or inherit the given type.
+     *
+     * @param quirkClass The super type of device quirk to retrieve.
+     * @return A device {@link Quirk} list of the provided type. An empty list is returned if it
+     * isn't found.
+     */
+    @SuppressWarnings("unchecked")
+    @NonNull
+    public static <T extends Quirk> List<T> getAll(@NonNull Class<T> quirkClass) {
+        List<Quirk> quirks = DeviceQuirksLoader.loadQuirks();
+        List<T> list = new ArrayList<>();
+        for (Quirk quirk : quirks) {
+            if (quirkClass.isAssignableFrom(quirk.getClass())) {
+                list.add((T) quirk);
+            }
+        }
+        return list;
+    }
 }
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index f9b51e6..4ce01dd 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -58,7 +58,7 @@
         // Outside of androidx this is resolved via constraint added to lifecycle-common,
         // but it doesn't work in androidx.
         // See aosp/1804059
-        androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
+        androidTestImplementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.junit)
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index c6bf227..b2e864f 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -48,8 +48,8 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation(project(":lifecycle:lifecycle-common-java8"))
-    implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+    implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+    implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     implementation(project(":paging:paging-compose"))
 
     implementation(libs.kotlinStdlib)
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 4640322..8b2e8c1 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -29,7 +29,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
     implementation(project(":compose:foundation:foundation-layout"))
     implementation(project(":compose:foundation:foundation"))
     implementation(project(":compose:material:material"))
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index e2398cd..c6afe8e 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -58,7 +58,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
 }
 
 // We want to publish a release APK of this project for the Compose Material Catalog
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index 8a99350..ac923ef 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -60,7 +60,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+    androidTestImplementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
 }
 
 androidx {
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 4f1ce49..47ef466 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -49,7 +49,7 @@
         // Outside of androidx this is resolved via constraint added to lifecycle-common,
         // but it doesn't work in androidx.
         // See aosp/1804059
-        implementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
+        implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
         implementation(libs.testCore)
         implementation(libs.testRules)
 
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 15776c0..f8affb0 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -107,8 +107,8 @@
                 // Outside of androidx this is resolved via constraint added to lifecycle-common,
                 // but it doesn't work in androidx.
                 // See aosp/1804059
-                implementation(project(":lifecycle:lifecycle-common-java8"))
-                implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+                implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
 
                 implementation(libs.junit)
                 implementation(libs.testRunner)
diff --git a/external/paparazzi/paparazzi/build.gradle b/external/paparazzi/paparazzi/build.gradle
index 932b5f1..3d02e4b 100644
--- a/external/paparazzi/paparazzi/build.gradle
+++ b/external/paparazzi/paparazzi/build.gradle
@@ -6,9 +6,11 @@
     id("AndroidXPlugin")
     id("kotlin")
     id("com.google.devtools.ksp")
+    id("AndroidXComposePlugin")
 }
 
 androidx.configureAarAsJarForConfiguration("compileOnly")
+androidx.configureAarAsJarForConfiguration("testImplementation")
 
 dependencies {
     api("androidx.annotation:annotation:1.3.0")
@@ -44,6 +46,7 @@
     ksp(libs.moshiCodeGen)
 
     testImplementation(libs.assertj)
+    testImplementationAarAsJar("androidx.compose.runtime:runtime:1.2.1")
 }
 
 androidx {
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index ec2705e..6bf32f6 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -393,6 +393,16 @@
 
 }
 
+package androidx.glance.session {
+
+  public final class SessionKt {
+  }
+
+  public final class SessionManagerKt {
+  }
+
+}
+
 package androidx.glance.state {
 
   public interface GlanceStateDefinition<T> {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index ec2705e..6bf32f6 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -393,6 +393,16 @@
 
 }
 
+package androidx.glance.session {
+
+  public final class SessionKt {
+  }
+
+  public final class SessionManagerKt {
+  }
+
+}
+
 package androidx.glance.state {
 
   public interface GlanceStateDefinition<T> {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index ec2705e..6bf32f6 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -393,6 +393,16 @@
 
 }
 
+package androidx.glance.session {
+
+  public final class SessionKt {
+  }
+
+  public final class SessionManagerKt {
+  }
+
+}
+
 package androidx.glance.state {
 
   public interface GlanceStateDefinition<T> {
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 4fb08c4..30e291c 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -38,6 +38,8 @@
     api("androidx.datastore:datastore-preferences:1.0.0")
 
     implementation("androidx.annotation:annotation:1.1.0")
+    implementation("androidx.work:work-runtime:2.7.1")
+    implementation("androidx.work:work-runtime-ktx:2.7.1")
     implementation(libs.kotlinStdlib)
     implementation(project(":compose:runtime:runtime"))
 
@@ -55,6 +57,7 @@
     testImplementation("androidx.datastore:datastore-core:1.0.0")
     testImplementation("androidx.datastore:datastore-preferences-core:1.0.0")
     testImplementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+    testImplementation("androidx.work:work-testing:2.7.1")
     testImplementation("com.google.android.material:material:1.6.0")
 }
 
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
index dc4753f..d2d086e 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.unit.DpSize
 import androidx.datastore.preferences.core.Preferences
@@ -40,8 +42,7 @@
  * Local view state, defined in surface implementation. A customizable store for view specific state
  * data.
  */
-val LocalState =
-    staticCompositionLocalOf<Any?> { null }
+val LocalState = compositionLocalOf<Any?> { null }
 
 /**
  * Unique Id for the glance view being generated by the current composition.
@@ -55,7 +56,12 @@
  * @return the current store of the provided type [T]
  */
 @Composable
-inline fun <reified T> currentState(): T = LocalState.current as T
+inline fun <reified T> currentState(): T = LocalState.current.let {
+    when (it) {
+        is State<*> -> it.value as T
+        else -> it as T
+    }
+}
 
 /**
  * Retrieves the current [Preferences] value of the provided [Preferences.Key] from the current
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt
new file mode 100644
index 0000000..7a9bba7
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import androidx.compose.runtime.snapshots.Snapshot
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+
+/**
+ * Mechanism for glance sessions to start a monitor of global snapshot state writes in order to
+ * schedule periodic dispatch of apply notifications.
+ * Sessions should call [ensureStarted] during setup to initialize periodic global snapshot
+ * notifications (which are necessary in order for recompositions to be scheduled in response to
+ * state changes). These will be sent on Dispatchers.Default.
+ * This is based on [androidx.compose.ui.platform.GlobalSnapshotManager].
+ */
+internal object GlobalSnapshotManager {
+    private val started = AtomicBoolean(false)
+
+    fun ensureStarted() {
+        if (started.compareAndSet(false, true)) {
+            val channel = Channel<Unit>(Channel.CONFLATED)
+            CoroutineScope(Dispatchers.Default).launch {
+                channel.consumeEach {
+                    Snapshot.sendApplyNotifications()
+                }
+            }
+            Snapshot.registerGlobalWriteObserver {
+                channel.trySend(Unit)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt
new file mode 100644
index 0000000..795030c
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/InteractiveFrameClock.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.BroadcastFrameClock
+import androidx.compose.runtime.MonotonicFrameClock
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeoutOrNull
+
+/**
+ * A frame clock implementation that supports interactive mode.
+ *
+ * By default, this frame clock sends frames at its baseline rate. When startInteractive() is
+ * called, the frame clock sends frames at its interactive rate so that awaiters can respond more
+ * quickly to user interactions. After the interactive timeout is passed, the frame rate is reset to
+ * its baseline.
+ */
+internal class InteractiveFrameClock(
+    private val scope: CoroutineScope,
+    private val baselineHz: Int = 5,
+    private val interactiveHz: Int = 20,
+    private val interactiveTimeoutMs: Long = 5_000,
+    private val nanoTime: () -> Long = { System.nanoTime() }
+) : MonotonicFrameClock {
+    companion object {
+        private const val NANOSECONDS_PER_SECOND = 1_000_000_000L
+        private const val NANOSECONDS_PER_MILLISECOND = 1_000_000L
+        private const val TAG = "InteractiveFrameClock"
+        private const val DEBUG = false
+    }
+    private val frameClock: BroadcastFrameClock = BroadcastFrameClock { onNewAwaiters() }
+    private val lock = Any()
+    private var currentHz = baselineHz
+    private var lastFrame = 0L
+    private var interactiveCoroutine: CancellableContinuation<Unit>? = null
+
+    /**
+     * Set the frame rate to [interactiveHz]. After [interactiveTimeoutMs] has passed, the frame
+     * rate is reset to [baselineHz]. If this function is called concurrently with itself, the
+     * previous call is cancelled and a new interactive period is started.
+     */
+    suspend fun startInteractive() = withTimeoutOrNull(interactiveTimeoutMs) {
+        stopInteractive()
+        suspendCancellableCoroutine { co ->
+            if (DEBUG) Log.d(TAG, "Starting interactive mode at ${interactiveHz}hz")
+            synchronized(lock) {
+                currentHz = interactiveHz
+                interactiveCoroutine = co
+            }
+
+            co.invokeOnCancellation {
+                if (DEBUG) Log.d(TAG, "Resetting frame rate to baseline at ${baselineHz}hz")
+                synchronized(lock) {
+                    currentHz = baselineHz
+                    interactiveCoroutine = null
+                }
+            }
+        }
+    }
+
+    /**
+     * Cancel the call to startInteractive() if running, and reset the frame rate to baseline.
+     */
+    fun stopInteractive() {
+        synchronized(lock) {
+            interactiveCoroutine?.cancel()
+        }
+    }
+
+    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+        if (DEBUG) Log.d(TAG, "received frame to run")
+        return frameClock.withFrameNanos(onFrame)
+    }
+
+    private fun onNewAwaiters() {
+        val now = nanoTime()
+        val period: Long
+        val minPeriod: Long
+        synchronized(lock) {
+            period = now - lastFrame
+            minPeriod = NANOSECONDS_PER_SECOND / currentHz
+        }
+        if (period >= minPeriod) {
+            sendFrame(now)
+        } else {
+            scope.launch {
+                delay((minPeriod - period) / NANOSECONDS_PER_MILLISECOND)
+                sendFrame(nanoTime())
+            }
+        }
+    }
+
+    private fun sendFrame(now: Long) {
+        if (DEBUG) Log.d(TAG, "Sending next frame")
+        frameClock.sendFrame(now)
+        synchronized(lock) {
+            lastFrame = now
+        }
+    }
+
+    @VisibleForTesting
+    internal fun currentHz() = synchronized(lock) { currentHz }
+}
\ No newline at end of file
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
new file mode 100644
index 0000000..277cfc89
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.glance.EmittableWithChildren
+import androidx.glance.GlanceComposable
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ClosedReceiveChannelException
+
+typealias SetContentFn = suspend (@Composable @GlanceComposable () -> Unit) -> Unit
+
+/**
+ * [Session] is implemented by Glance surfaces in order to provide content for the
+ * composition and process the results of recomposition.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class Session(val key: String) {
+    private val eventChannel = Channel<Any>(Channel.UNLIMITED)
+
+    /**
+     *  Create the [EmittableWithChildren] that will be used as the [androidx.glance.Applier] root.
+     */
+    abstract fun createRootEmittable(): EmittableWithChildren
+
+    /**
+     * Provide the Glance composable to be run in the [androidx.compose.runtime.Composition].
+     */
+    abstract suspend fun provideGlance(
+        context: Context,
+        setContent: SetContentFn,
+    )
+
+    /**
+     * Process the Emittable tree that results from the running the composable provided by
+     * [provideGlance].
+     *
+     * This will also be called for the results of future recompositions.
+     */
+    abstract suspend fun processEmittableTree(
+        context: Context,
+        root: EmittableWithChildren,
+    )
+
+    /**
+     * Process an event that was sent to this session.
+     */
+    abstract suspend fun processEvent(
+        context: Context,
+        event: Any,
+    )
+
+    /**
+     * Enqueues an [event] to be processed by the session.
+     *
+     * These requests may be processed by calling [receiveEvents].
+     */
+    suspend fun sendEvent(event: Any) {
+        eventChannel.send(event)
+    }
+
+    /**
+     * Process incoming events, additionally running [block] for each event that is received.
+     *
+     * This function suspends until [close] is called.
+     */
+    suspend fun receiveEvents(context: Context, block: (Any) -> Unit) {
+        try {
+            for (event in eventChannel) {
+                block(event)
+                processEvent(context, event)
+            }
+        } catch (_: ClosedReceiveChannelException) {}
+    }
+
+    suspend fun close() {
+        eventChannel.close()
+        onClose()
+    }
+
+    open suspend fun onClose() {}
+}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
new file mode 100644
index 0000000..cb4c94b
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.work.Constraints
+import androidx.work.ExistingWorkPolicy
+import androidx.work.ListenableWorker
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.await
+import androidx.work.workDataOf
+import java.util.concurrent.TimeUnit
+
+/**
+ * [SessionManager] is the entrypoint for Glance surfaces to start a session worker that will handle
+ * their composition.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SessionManager {
+    /**
+     * Start a session for the Glance in [session].
+     */
+    suspend fun startSession(context: Context, session: Session)
+
+    /**
+     * Closes the channel for the session corresponding to [key].
+     */
+    suspend fun closeSession(key: String)
+
+    /**
+     * Returns true if a session is active with the given [key].
+     */
+    suspend fun isSessionRunning(context: Context, key: String): Boolean
+
+    /**
+     * Gets the session corresponding to [key] if it exists
+     */
+    fun getSession(key: String): Session?
+
+    val keyParam: String
+        get() = "KEY"
+}
+
+/** @suppress */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+val GlanceSessionManager: SessionManager = SessionManagerImpl(SessionWorker::class.java)
+
+internal class SessionManagerImpl(
+    private val workerClass: Class<out ListenableWorker>
+) : SessionManager {
+    private val sessions = mutableMapOf<String, Session>()
+    companion object {
+        private const val TAG = "GlanceSessionManager"
+        private const val DEBUG = false
+    }
+
+    override suspend fun startSession(context: Context, session: Session) {
+        if (DEBUG) Log.d(TAG, "startSession(${session.key})")
+        synchronized(sessions) {
+            sessions.put(session.key, session)
+        }?.close()
+        val workRequest = OneTimeWorkRequest.Builder(workerClass)
+            .setInputData(
+                workDataOf(
+                    keyParam to session.key
+                )
+            )
+            .build()
+        WorkManager.getInstance(context)
+            .enqueueUniqueWork(session.key, ExistingWorkPolicy.REPLACE, workRequest)
+            .result.await()
+        enqueueDelayedWorker(context)
+    }
+
+    override fun getSession(key: String): Session? = synchronized(sessions) {
+        sessions[key]
+    }
+
+    override suspend fun isSessionRunning(context: Context, key: String) =
+        (WorkManager.getInstance(context).getWorkInfosForUniqueWork(key).await()
+            .any { it.state == WorkInfo.State.RUNNING } && synchronized(sessions) {
+            sessions.containsKey(key)
+        }).also {
+            if (DEBUG) Log.d(TAG, "isSessionRunning($key) == $it")
+        }
+
+    override suspend fun closeSession(key: String) {
+        if (DEBUG) Log.d(TAG, "closeSession($key)")
+        synchronized(sessions) {
+            sessions.remove(key)
+        }?.close()
+    }
+
+    /**
+     * Workaround worker to fix b/119920965
+     */
+    private fun enqueueDelayedWorker(context: Context) {
+        WorkManager.getInstance(context).enqueueUniqueWork(
+            "sessionWorkerKeepEnabled",
+            ExistingWorkPolicy.KEEP,
+            OneTimeWorkRequest.Builder(workerClass)
+                .setInitialDelay(10 * 365, TimeUnit.DAYS)
+                .setConstraints(
+                    Constraints.Builder()
+                        .setRequiresCharging(true)
+                        .build()
+                )
+                .build()
+        )
+    }
+}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
new file mode 100644
index 0000000..b97b319
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.Recomposer
+import androidx.glance.Applier
+import androidx.glance.EmittableWithChildren
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/**
+ * [SessionWorker] handles composition for a particular Glanceable.
+ *
+ * This worker runs the [Session] it acquires from [SessionManager] for the key given in the worker
+ * params. The worker then sets up and runs a composition, then provides the resulting UI tree
+ * (and those of successive recompositions) to [Session.processEmittableTree]. After the initial
+ * composition, the worker blocks on [Session.receiveEvents] until [Session.close] is called.
+ */
+internal class SessionWorker(
+    appContext: Context,
+    params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+    @VisibleForTesting
+    internal var sessionManager: SessionManager = GlanceSessionManager
+
+    companion object {
+        private const val TAG = "GlanceSessionWorker"
+        private const val DEBUG = false
+    }
+
+    override suspend fun doWork(): Result = coroutineScope {
+        val frameClock = InteractiveFrameClock(this)
+        val key =
+            inputData.getString(sessionManager.keyParam) ?: return@coroutineScope Result.failure()
+        val session = requireNotNull(sessionManager.getSession(key)) {
+            "No session available to key $key"
+        }
+
+        if (DEBUG) Log.d(TAG, "Setting up composition for ${session.key}")
+        GlobalSnapshotManager.ensureStarted()
+        val root = session.createRootEmittable()
+        val recomposer = Recomposer(coroutineContext)
+        val composition = Composition(Applier(root), recomposer)
+        val contentReady = MutableStateFlow(false)
+        val uiReady = MutableStateFlow(false)
+        var contentCoroutine: CancellableContinuation<Unit>? = null
+        launch {
+            session.provideGlance(applicationContext) { content ->
+                contentCoroutine?.cancel()
+                suspendCancellableCoroutine { co ->
+                    contentCoroutine = co
+                    composition.setContent(content)
+                    contentReady.tryEmit(true)
+                }
+            }
+        }
+        launch {
+            contentReady.first { it }
+            withContext(frameClock) { recomposer.runRecomposeAndApplyChanges() }
+        }
+        launch {
+            contentReady.first { it }
+            recomposer.currentState.collect { state ->
+                if (DEBUG) Log.d(TAG, "Recomposer(${session.key}): currentState=$state")
+                when (state) {
+                    Recomposer.State.Idle -> {
+                        if (DEBUG) Log.d(TAG, "UI tree ready (${session.key})")
+                        session.processEmittableTree(
+                            applicationContext,
+                            root.copy() as EmittableWithChildren
+                        )
+                        uiReady.emit(true)
+                    }
+                    Recomposer.State.ShutDown -> cancel()
+                    else -> {}
+                }
+            }
+        }
+
+        uiReady.first { it }
+        session.receiveEvents(applicationContext) {
+            if (DEBUG) Log.d(TAG, "processing event for ${session.key}")
+            launch { frameClock.startInteractive() }
+        }
+
+        composition.dispose()
+        contentCoroutine?.resume(Unit)
+        frameClock.stopInteractive()
+        recomposer.close()
+        recomposer.join()
+        return@coroutineScope Result.success()
+    }
+}
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt
new file mode 100644
index 0000000..2c55ac0
--- /dev/null
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/InteractiveFrameClockTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class InteractiveFrameClockTest {
+    private lateinit var clock: InteractiveFrameClock
+
+    companion object {
+        private const val NANOSECONDS_PER_MILLISECOND = 1_000_000
+        private const val NANOSECONDS_PER_SECOND = 1_000_000_000L
+        private const val BASELINE_HZ = 5
+        private const val MIN_BASELINE_PERIOD = NANOSECONDS_PER_SECOND / BASELINE_HZ
+        private const val INTERACTIVE_HZ = 20
+        private const val MIN_INTERACTIVE_PERIOD = NANOSECONDS_PER_SECOND / INTERACTIVE_HZ
+        private const val INTERACTIVE_TIMEOUT = 5_000L
+    }
+    @Test
+    fun sendFramesAtBaselineHz() = runTest {
+        advanceTimeBy(System.currentTimeMillis())
+        clock = InteractiveFrameClock(this, BASELINE_HZ, INTERACTIVE_HZ, INTERACTIVE_TIMEOUT) {
+            currentTime * NANOSECONDS_PER_MILLISECOND
+        }
+        // awaiter1 will be sent immediately, awaiter2 & awaiter3 will be sent together at least
+        // 1/5th of a second later.
+        val awaiter1 = async { clock.withFrameNanos { it } }
+        val awaiter2 = async { clock.withFrameNanos { it } }
+        val awaiter3 = async { clock.withFrameNanos { it } }
+        advanceUntilIdle()
+        val frame1 = awaiter1.await()
+        val frame2 = awaiter2.await()
+        val frame3 = awaiter3.await()
+        assertThat(frame2 - frame1).isEqualTo(MIN_BASELINE_PERIOD)
+        assertThat(frame2).isEqualTo(frame3)
+    }
+
+    @Test
+    fun sendFramesAtInteractiveHz() = runTest {
+        advanceTimeBy(System.currentTimeMillis())
+        clock = InteractiveFrameClock(this, BASELINE_HZ, INTERACTIVE_HZ, INTERACTIVE_TIMEOUT) {
+            currentTime * NANOSECONDS_PER_MILLISECOND
+        }
+        launch { clock.startInteractive() }
+        // awaiter1 will be sent immediately, awaiter2 & awaiter3 will be sent together at least
+        // 1/20th of a second later.
+        val awaiter1 = async { clock.withFrameNanos { it } }
+        val awaiter2 = async { clock.withFrameNanos { it } }
+        val awaiter3 = async { clock.withFrameNanos { it } }
+        advanceUntilIdle()
+        val frame1 = awaiter1.await()
+        val frame2 = awaiter2.await()
+        val frame3 = awaiter3.await()
+        assertThat(frame2 - frame1).isEqualTo(MIN_INTERACTIVE_PERIOD)
+        assertThat(frame2).isEqualTo(frame3)
+    }
+
+    @Test
+    fun interactiveModeTimeout() = runTest {
+        advanceTimeBy(System.currentTimeMillis())
+        clock = InteractiveFrameClock(this, BASELINE_HZ, INTERACTIVE_HZ, INTERACTIVE_TIMEOUT) {
+            currentTime * NANOSECONDS_PER_MILLISECOND
+        }
+        launch { clock.startInteractive() }
+        yield()
+        assertThat(clock.currentHz()).isEqualTo(INTERACTIVE_HZ)
+        advanceTimeBy(INTERACTIVE_TIMEOUT / 2)
+        assertThat(clock.currentHz()).isEqualTo(INTERACTIVE_HZ)
+        advanceTimeBy(1 + INTERACTIVE_TIMEOUT / 2)
+        assertThat(clock.currentHz()).isEqualTo(BASELINE_HZ)
+    }
+
+    @Test
+    fun stopInteractive() = runTest {
+        advanceTimeBy(System.currentTimeMillis())
+        clock = InteractiveFrameClock(this, BASELINE_HZ, INTERACTIVE_HZ, INTERACTIVE_TIMEOUT) {
+            currentTime * NANOSECONDS_PER_MILLISECOND
+        }
+        val interactiveJob = launch { clock.startInteractive() }
+        yield()
+        assertThat(clock.currentHz()).isEqualTo(INTERACTIVE_HZ)
+        clock.stopInteractive()
+        assertThat(interactiveJob.isCompleted)
+        assertThat(clock.currentHz()).isEqualTo(BASELINE_HZ)
+    }
+}
\ No newline at end of file
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
new file mode 100644
index 0000000..a106652
--- /dev/null
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package androidx.glance.session
+
+import android.content.Context
+import androidx.glance.EmittableWithChildren
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.CoroutineWorker
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import androidx.work.testing.WorkManagerTestInitHelper
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SessionManagerImplTest {
+    private val key = "KEY"
+    private val session = object : Session(key) {
+        override fun createRootEmittable(): EmittableWithChildren {
+            TODO("Not yet implemented")
+        }
+
+        override suspend fun provideGlance(context: Context, setContent: SetContentFn) {
+            TODO("Not yet implemented")
+        }
+
+        override suspend fun processEmittableTree(context: Context, root: EmittableWithChildren) {
+            TODO("Not yet implemented")
+        }
+
+        override suspend fun processEvent(context: Context, event: Any) {
+            TODO("Not yet implemented")
+        }
+    }
+    private lateinit var context: Context
+    private lateinit var sessionManager: SessionManagerImpl
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+        WorkManagerTestInitHelper.initializeTestWorkManager(context)
+        sessionManager = SessionManagerImpl(TestWorker::class.java)
+    }
+
+    @After
+    fun cleanUp() {
+        WorkManager.getInstance(context).cancelAllWork()
+    }
+
+    @Test
+    fun startSession() = runTest {
+        assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
+        sessionManager.startSession(context, session)
+        assertThat(sessionManager.isSessionRunning(context, key)).isTrue()
+        assertThat(sessionManager.getSession(key)).isSameInstanceAs(session)
+    }
+
+    @Test
+    fun closeSession() = runTest {
+        sessionManager.startSession(context, session)
+        assertThat(sessionManager.isSessionRunning(context, key)).isTrue()
+        sessionManager.closeSession(key)
+        assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
+        assertThat(sessionManager.getSession(key)).isNull()
+    }
+}
+
+class TestWorker(context: Context, workerParams: WorkerParameters) :
+    CoroutineWorker(context, workerParams) {
+    override suspend fun doWork(): Result {
+        suspendCoroutine<Unit> {}
+        return Result.success()
+    }
+}
\ No newline at end of file
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
new file mode 100644
index 0000000..bd78885
--- /dev/null
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.session
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.glance.EmittableWithChildren
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.layout.Box
+import androidx.glance.layout.EmittableBox
+import androidx.glance.text.EmittableText
+import androidx.glance.text.Text
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.Data
+import androidx.work.ListenableWorker.Result
+import androidx.work.testing.TestListenableWorkerBuilder
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SessionWorkerTest {
+    private val sessionManager = TestSessionManager()
+    private lateinit var context: Context
+    private lateinit var worker: SessionWorker
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+        worker = TestListenableWorkerBuilder<SessionWorker>(context)
+            .setInputData(Data(mapOf(sessionManager.keyParam to SESSION_KEY)))
+            .build()
+            .also {
+                it.sessionManager = sessionManager
+            }
+    }
+
+    @Test
+    fun createSessionWorker() = runBlocking {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+        sessionManager.startSession(context)
+        sessionManager.closeSession()
+    }
+
+    @Test
+    fun sessionWorkerRunsComposition() = runBlocking {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val root = sessionManager.startSession(context) {
+            Box {
+                Text("Hello World")
+            }
+        }.first()
+        val box = assertIs<EmittableBox>(root.children.single())
+        val text = assertIs<EmittableText>(box.children.single())
+        assertThat(text.text).isEqualTo("Hello World")
+        sessionManager.closeSession()
+    }
+
+    @Test
+    fun sessionWorkerCallsProvideGlance(): Unit = runBlocking {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+        sessionManager.startSession(context).first()
+        val session = assertIs<TestSession>(sessionManager.getSession(SESSION_KEY))
+        assertThat(session.provideGlanceCalled).isEqualTo(1)
+        sessionManager.closeSession()
+    }
+
+    @Test
+    fun sessionWorkerStateChangeTriggersRecomposition() = runBlocking {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val state = mutableStateOf("Hello World")
+        val uiFlow = sessionManager.startSession(context) {
+                Text(state.value)
+        }
+        uiFlow.first().let { root ->
+            val text = assertIs<EmittableText>(root.children.single())
+            assertThat(text.text).isEqualTo("Hello World")
+        }
+
+        state.value = "Hello Earth"
+        uiFlow.first().let { root ->
+            val text = assertIs<EmittableText>(root.children.single())
+            assertThat(text.text).isEqualTo("Hello Earth")
+        }
+        sessionManager.closeSession()
+    }
+
+    @Test
+    fun sessionWorkerReceivesActions() = runBlocking {
+        launch {
+            val result = worker.doWork()
+            assertThat(result).isEqualTo(Result.success())
+        }
+
+        val state = mutableStateOf("Hello World")
+        val uiFlow = sessionManager.startSession(context) {
+            Text(state.value)
+        }
+        uiFlow.first().let { root ->
+            val text = assertIs<EmittableText>(root.children.single())
+            assertThat(text.text).isEqualTo("Hello World")
+        }
+        val session = assertNotNull(sessionManager.getSession(SESSION_KEY))
+        session.sendEvent {
+            state.value = "Hello Earth"
+        }
+        uiFlow.first().let { root ->
+            val text = assertIs<EmittableText>(root.children.single())
+            assertThat(text.text).isEqualTo("Hello Earth")
+        }
+        sessionManager.closeSession()
+    }
+}
+
+private const val SESSION_KEY = "123"
+
+class TestSessionManager : SessionManager {
+    private val sessions = mutableMapOf<String, Session>()
+
+    suspend fun startSession(
+        context: Context,
+        content: @GlanceComposable @Composable () -> Unit = {}
+    ) = MutableSharedFlow<EmittableWithChildren>().also { flow ->
+        startSession(context, TestSession(onUiFlow = flow, content = content))
+    }
+
+    suspend fun closeSession() {
+        closeSession(SESSION_KEY)
+    }
+
+    override suspend fun startSession(context: Context, session: Session) {
+        sessions[session.key] = session
+    }
+
+    override suspend fun closeSession(key: String) {
+        sessions[key]?.close()
+    }
+
+    override suspend fun isSessionRunning(context: Context, key: String): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun getSession(key: String): Session? = sessions[key]
+}
+
+class TestSession(
+    key: String = SESSION_KEY,
+    val onUiFlow: MutableSharedFlow<EmittableWithChildren>? = null,
+    val content: @GlanceComposable @Composable () -> Unit = {},
+) : Session(key) {
+    override fun createRootEmittable() = object : EmittableWithChildren() {
+        override var modifier: GlanceModifier = GlanceModifier
+        override fun copy() = this
+        override fun toString() = "EmittableRoot(children=[\n${childrenToString()}\n])"
+    }
+
+    var provideGlanceCalled = 0
+    override suspend fun provideGlance(
+        context: Context,
+        setContent: SetContentFn
+    ) {
+        provideGlanceCalled++
+        setContent(content)
+    }
+
+    override suspend fun processEmittableTree(context: Context, root: EmittableWithChildren) {
+        onUiFlow?.emit(root)
+    }
+
+    override suspend fun processEvent(context: Context, event: Any) {
+        require(event is Function0<*>)
+        event.invoke()
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLManager.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLManager.kt
index 59c43b1f..37438b5 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLManager.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLManager.kt
@@ -234,9 +234,13 @@
      * Helper method to query properties of the given surface
      */
     private fun querySurface(surface: EGLSurface) {
-        val resultArray = mQueryResult ?: IntArray(1).also { mQueryResult = it }
-        if (eglSpec.eglQuerySurface(surface, EGL14.EGL_RENDER_BUFFER, resultArray, 0)) {
-            mIsSingleBuffered = resultArray[0] == EGL14.EGL_SINGLE_BUFFER
+        if (surface == EGL14.EGL_NO_SURFACE) {
+            mIsSingleBuffered = false
+        } else {
+            val resultArray = mQueryResult ?: IntArray(1).also { mQueryResult = it }
+            if (eglSpec.eglQuerySurface(surface, EGL14.EGL_RENDER_BUFFER, resultArray, 0)) {
+                mIsSingleBuffered = resultArray[0] == EGL14.EGL_SINGLE_BUFFER
+            }
         }
     }
 
diff --git a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppAuthenticatorTest.java b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppAuthenticatorTest.java
index cd3ad3e..1e89cdf 100644
--- a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppAuthenticatorTest.java
+++ b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppAuthenticatorTest.java
@@ -28,10 +28,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
@@ -40,6 +42,9 @@
 // API Level 28 introduced signing key rotation, so run the tests with and without rotation support.
 @Config(minSdk = 27, maxSdk = 28)
 public class AppAuthenticatorTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     private static final String TEST_PACKAGE = "com.android.app1";
     private static final String TEST_PERMISSION = "androidx.security.app.authenticator"
             + ".TEST_PERMISSION";
@@ -53,10 +58,8 @@
     @Mock
     private AppSignatureVerifier mMockAppSignatureVerifier;
 
-    @SuppressWarnings("deprecation") // b/251210952
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
         Context context = ApplicationProvider.getApplicationContext();
         mAppAuthenticator = AppAuthenticator.createFromResource(context,
                 R.xml.all_supported_elements_and_attributes);
diff --git a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
index 3219ae9..bbacb0dc 100644
--- a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
+++ b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
@@ -32,10 +32,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
 import org.robolectric.shadow.api.Shadow;
@@ -54,6 +56,9 @@
 // API levels < 28.
 @SuppressWarnings("deprecation")
 public class AppSignatureVerifierTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     private static final String TEST_PACKAGE_NAME = "com.android.testapp";
     private static final String TEST_PERMISSION_NAME = "com.android.testapp.TEST_PERMISSION";
     private static final long LAST_UPDATE_TIME = 1234;
@@ -76,10 +81,8 @@
 
     private AppSignatureVerifierTestBuilder mBuilder;
 
-    @SuppressWarnings("deprecation") // b/251210952
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         mBuilder = new AppSignatureVerifierTestBuilder(mMockContext);
     }
diff --git a/security/security-biometric/OWNERS b/security/security-biometric/OWNERS
index e3e5d8e..8915a5f 100644
--- a/security/security-biometric/OWNERS
+++ b/security/security-biometric/OWNERS
@@ -1,2 +1,3 @@
[email protected]
[email protected]
[email protected]
 [email protected]
diff --git a/security/security-crypto-ktx/OWNERS b/security/security-crypto-ktx/OWNERS
index 7cc0932..8915a5f 100644
--- a/security/security-crypto-ktx/OWNERS
+++ b/security/security-crypto-ktx/OWNERS
@@ -1,2 +1,3 @@
[email protected]
[email protected]
\ No newline at end of file
[email protected]
[email protected]
[email protected]
diff --git a/security/security-crypto/OWNERS b/security/security-crypto/OWNERS
index f856cfb..48f2be8 100644
--- a/security/security-crypto/OWNERS
+++ b/security/security-crypto/OWNERS
@@ -1 +1,2 @@
[email protected]
\ No newline at end of file
[email protected]
[email protected]
diff --git a/settings.gradle b/settings.gradle
index dbc189b..15db17c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -856,7 +856,7 @@
 includeProject(":test:ext:junit-gtest", [BuildType.NATIVE])
 includeProject(":test:integration-tests:junit-gtest-test", [BuildType.NATIVE])
 includeProject(":test:screenshot:screenshot")
-includeProject(":test:screenshot:screenshot-layoutlib", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":test:screenshot:screenshot-layoutlib", [BuildType.COMPOSE])
 includeProject(":test:screenshot:screenshot-proto")
 includeProject(":test:uiautomator:uiautomator", [BuildType.MAIN])
 includeProject(":test:uiautomator:integration-tests:testapp", [BuildType.MAIN])
@@ -1035,8 +1035,8 @@
 includeProject(":external:libyuv", [BuildType.CAMERA])
 includeProject(":noto-emoji-compat-font", new File(externalRoot, "noto-fonts/emoji-compat"), [BuildType.MAIN])
 includeProject(":noto-emoji-compat-flatbuffers", new File(externalRoot, "noto-fonts/emoji-compat-flatbuffers"), [BuildType.MAIN])
-includeProject(":external:paparazzi:paparazzi")
-includeProject(":external:paparazzi:paparazzi-agent")
+includeProject(":external:paparazzi:paparazzi", [BuildType.COMPOSE])
+includeProject(":external:paparazzi:paparazzi-agent", [BuildType.COMPOSE])
 
 if (isAllProjects()) {
     includeProject(":docs-tip-of-tree")