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")