DO NOT MERGE: Merge remote-tracking branch 'aosp/androidx-platform-dev-temp' into merge_platform_dev
Fixes: 236262138
Change-Id: Ia5b278cc2d6989019e120e8ba5de10b35845c3ab
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 43b6a5a..d51c200 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/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("androidx.lifecycle:lifecycle-common-java8:2.5.0")
+ implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 71da6fa..b28eac7 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,10 +33,10 @@
api("androidx.core:core-ktx:1.1.0") {
because "Mirror activity dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0") {
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b2b7705..d086cea 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,10 +23,10 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api("androidx.core:core:1.8.0")
- api("androidx.lifecycle:lifecycle-runtime:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
+ api("androidx.lifecycle:lifecycle-runtime:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
api("androidx.savedstate:savedstate:1.2.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
implementation("androidx.tracing:tracing:1.0.0")
api(libs.kotlinStdlib)
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
index 5719451..069c76c 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 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.
@@ -20,3 +20,4 @@
* An activity for locales with a unique class name.
*/
public class LocalesActivityA extends LocalesUpdateActivity {}
+
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 9dc5995..c3e83b6 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -33,9 +33,12 @@
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_CANCELLED
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.io.StringReader
+import java.util.regex.Pattern
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -251,6 +254,70 @@
}
}
+ @Test
+ fun test_handshake_package_does_not_exist() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val response = perfettoCapture.enableAndroidxTracingPerfetto(
+ "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+ shouldProvideBinaries(testConfig.sdkDelivery)
+ )
+
+ assertThat(response).ignoringCase()
+ .contains("The broadcast to enable tracing was not received")
+ }
+
+ /**
+ * Unlike [test_handshake_package_does_not_exist], which uses [PerfettoCapture], this test
+ * uses a lower-level component [PerfettoHandshake].
+ */
+ @Test
+ fun test_handshake_framework_package_does_not_exist() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val handshake = PerfettoHandshake(
+ "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+ parseJsonMap = { emptyMap() },
+ Shell::executeCommand
+ )
+
+ // try
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+ assertThat(response.requiredVersion).isNull()
+ }
+
+ // try again
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+ assertThat(response.requiredVersion).isNull()
+ }
+ }
+
+ @Test
+ fun test_handshake_framework_parsing_error() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val parsingException = "I don't know how to JSON"
+ val handshake = PerfettoHandshake(
+ targetPackage,
+ parseJsonMap = { throw IllegalArgumentException(parsingException) },
+ Shell::executeCommand
+ )
+
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_ERROR_OTHER)
+ assertThat(response.requiredVersion).isNull()
+ assertThat(response.message).containsMatch(
+ "Exception occurred while trying to parse a response.*Error.*$parsingException"
+ .toPattern(Pattern.CASE_INSENSITIVE)
+ )
+ }
+ }
+
private fun enablePackage() {
scope.pressHome()
scope.startActivityAndWait()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 8a43bf6..bfc7189 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -39,7 +39,7 @@
const val ERROR_PRONE_TASK = "runErrorProne"
private const val ERROR_PRONE_CONFIGURATION = "errorprone"
-private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.4.0"
+private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.14.0"
private val log = Logging.getLogger("ErrorProneConfiguration")
fun Project.configureErrorProneForJava() {
@@ -128,6 +128,32 @@
"-XepExcludedPaths:.*/(build/generated|build/errorProne|external)/.*",
+ // Consider re-enabling the following checks. Disabled as part of
+ // error-prone upgrade
+ "-Xep:InlineMeSuggester:OFF",
+ "-Xep:UnusedVariable:OFF",
+ "-Xep:UnusedMethod:OFF",
+ "-Xep:NarrowCalculation:OFF",
+ "-Xep:LongDoubleConversion:OFF",
+ "-Xep:UnicodeEscape:OFF",
+ "-Xep:JavaUtilDate:OFF",
+ "-Xep:UnrecognisedJavadocTag:OFF",
+ "-Xep:ObjectEqualsForPrimitives:OFF",
+ "-Xep:UnnecessaryParentheses:OFF",
+ "-Xep:DoNotCallSuggester:OFF",
+ "-Xep:EqualsNull:OFF",
+ "-Xep:MalformedInlineTag:OFF",
+ "-Xep:MissingSuperCall:OFF",
+ "-Xep:ToStringReturnsNull:OFF",
+ "-Xep:ReturnValueIgnored:OFF",
+ "-Xep:MissingImplementsComparable:OFF",
+ "-Xep:EmptyTopLevelDeclaration:OFF",
+ "-Xep:InvalidThrowsLink:OFF",
+ "-Xep:StaticAssignmentOfThrowable:OFF",
+ "-Xep:DoNotClaimAnnotations:OFF",
+ "-Xep:AlreadyChecked:OFF",
+ "-Xep:StringSplitter:OFF",
+
// We allow inter library RestrictTo usage.
"-Xep:RestrictTo:OFF",
@@ -173,13 +199,13 @@
"-Xep:MissingFail:ERROR",
"-Xep:JavaLangClash:ERROR",
"-Xep:TypeParameterUnusedInFormals:ERROR",
- "-Xep:StringSplitter:ERROR",
+ // "-Xep:StringSplitter:ERROR", // disabled with upgrade to 2.14.0
"-Xep:ReferenceEquality:ERROR",
"-Xep:AssertionFailureIgnored:ERROR",
- "-Xep:UnnecessaryParentheses:ERROR",
+ // "-Xep:UnnecessaryParentheses:ERROR", // disabled with upgrade to 2.14.0
"-Xep:EqualsGetClass:ERROR",
- "-Xep:UnusedVariable:ERROR",
- "-Xep:UnusedMethod:ERROR",
+ // "-Xep:UnusedVariable:ERROR", // disabled with upgrade to 2.14.0
+ // "-Xep:UnusedMethod:ERROR", // disabled with upgrade to 2.14.0
"-Xep:UndefinedEquals:ERROR",
"-Xep:ThreadLocalUsage:ERROR",
"-Xep:FutureReturnValueIgnored:ERROR",
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
index ae4ae00..44e2abd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
@@ -16,6 +16,7 @@
package androidx.camera.core.impl;
+import android.hardware.camera2.CaptureRequest;
import android.util.ArrayMap;
import android.util.Pair;
@@ -40,6 +41,9 @@
private static final TagBundle EMPTY_TAGBUNDLE = new TagBundle(new ArrayMap<>());
+ private static final String USER_TAG_PREFIX = "android.hardware.camera2.CaptureRequest.setTag.";
+
+ private static final String CAMERAX_USER_TAG_PREFIX = USER_TAG_PREFIX + "CX";
/**
* Creates an empty TagBundle.
*
@@ -101,4 +105,24 @@
public Set<String> listKeys() {
return mTagMap.keySet();
}
+
+ /**
+ * Produces a string that can be used to identify CameraX usage in a Camera2
+ * {@link CaptureRequest}.
+ *
+ * <p>In Android 13 or later, Camera2 will log the string representation of any
+ * tag set on {@link CaptureRequest.Builder#setTag(Object)}. Since
+ * tag bundles are always set internally by CameraX as the tag in a capture
+ * request, the constant string value returned here can be used to identify
+ * usage of CameraX versus application usage of Camera2.
+ *
+ * <p>Note: Doesn't return an actual string representation of the tag bundle.
+ *
+ * @return Returns a constant string value used to identify usage of CameraX.
+ */
+ @NonNull
+ @Override
+ public final String toString() {
+ return CAMERAX_USER_TAG_PREFIX;
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
index e204317..3edf244 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
@@ -18,9 +18,15 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
-/** A provider that supplies OpenGL shader code. */
-interface ShaderProvider {
+/**
+ * A provider that supplies OpenGL shader code.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ShaderProvider {
/**
* Creates the fragment shader code with the given variable names.
@@ -34,14 +40,15 @@
* varying vec2 {$fragCoordsVarName};
* void main() {
* vec4 sampleColor = texture2D({$samplerVarName}, {$fragCoordsVarName});
- * gl_FragColor = vec4(sampleColor.r * 0.493 + sampleColor. g * 0.769 +
- * sampleColor.b * 0.289, sampleColor.r * 0.449 + sampleColor.g * 0.686 +
- * sampleColor.b * 0.268, sampleColor.r * 0.272 + sampleColor.g * 0.534 +
- * sampleColor.b * 0.131, 1.0);
+ * gl_FragColor = vec4(
+ * sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+ * sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+ * sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+ * 1.0);
* }
* }</pre>
*
- * @param samplerVarName the variable name of the samplerExternalOES.
+ * @param samplerVarName the variable name of the samplerExternalOES.
* @param fragCoordsVarName the variable name of the fragment coordinates.
* @return the shader code. Return null to use the default shader.
*/
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
index 616fb90..85572f1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
@@ -19,7 +19,6 @@
import android.os.Build
import androidx.camera.core.SurfaceEffect.PREVIEW
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
-import androidx.camera.testing.fakes.FakeSurfaceEffect
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,7 +41,11 @@
@Test(expected = IllegalArgumentException::class)
fun addMoreThanOnePreviewEffect_throwsException() {
- val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+ val surfaceEffect = object : SurfaceEffect {
+ override fun onInputSurface(request: SurfaceRequest) {}
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+ }
EffectBundle.Builder(mainThreadExecutor())
.addEffect(PREVIEW, surfaceEffect)
.addEffect(PREVIEW, surfaceEffect)
@@ -51,7 +54,11 @@
@Test
fun addPreviewEffect_hasPreviewEffect() {
// Arrange.
- val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+ val surfaceEffect = object : SurfaceEffect {
+ override fun onInputSurface(request: SurfaceRequest) {}
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+ }
// Act.
val effectBundle = EffectBundle.Builder(mainThreadExecutor())
.addEffect(PREVIEW, surfaceEffect)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 0cae6de..1be7cf1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -42,7 +42,6 @@
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
import androidx.camera.testing.fakes.FakeUseCase
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -72,6 +71,8 @@
private lateinit var appSurface: Surface
private lateinit var appSurfaceTexture: SurfaceTexture
+ private lateinit var effectSurface: Surface
+ private lateinit var effectSurfaceTexture: SurfaceTexture
private lateinit var camera: FakeCamera
private lateinit var cameraXConfig: CameraXConfig
private lateinit var context: Context
@@ -81,6 +82,8 @@
fun setUp() {
appSurfaceTexture = SurfaceTexture(0)
appSurface = Surface(appSurfaceTexture)
+ effectSurfaceTexture = SurfaceTexture(0)
+ effectSurface = Surface(effectSurfaceTexture)
camera = FakeCamera()
val cameraFactoryProvider =
@@ -103,6 +106,8 @@
fun tearDown() {
appSurfaceTexture.release()
appSurface.release()
+ effectSurfaceTexture.release()
+ effectSurface.release()
with(cameraUseCaseAdapter) {
this?.removeUseCases(useCases)
}
@@ -217,10 +222,27 @@
@Test
fun bindAndUnbindPreview_surfacesPropagated() {
// Arrange.
- val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
+ var surfaceOutputReceived: SurfaceOutput? = null
+ var effectSurfaceReadyToRelease = false
+ var isEffectReleased = false
+ val surfaceEffect = object : SurfaceEffectInternal {
+ override fun onInputSurface(request: SurfaceRequest) {
+ request.provideSurface(effectSurface, mainThreadExecutor()) {
+ effectSurfaceReadyToRelease = true
+ }
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ surfaceOutputReceived = surfaceOutput
+ }
+
+ override fun release() {
+ isEffectReleased = true
+ }
+ }
// Act: create pipeline in Preview and provide Surface.
- val preview = createPreviewPipelineAndAttachEffect(effect)
+ val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
val surfaceRequest = preview.mCurrentSurfaceRequest!!
var appSurfaceReadyToRelease = false
surfaceRequest.provideSurface(appSurface, mainThreadExecutor()) {
@@ -229,26 +251,30 @@
shadowOf(getMainLooper()).idle()
// Assert: surfaceOutput received.
- assertThat(effect.surfaceOutput).isNotNull()
- assertThat(effect.isReleased).isFalse()
- assertThat(effect.isOutputSurfaceRequestedToClose).isFalse()
- assertThat(effect.isInputSurfaceReleased).isFalse()
+ assertThat(surfaceOutputReceived).isNotNull()
+ var requestedToReleaseOutputSurface = false
+ surfaceOutputReceived!!.getSurface(mainThreadExecutor()) {
+ requestedToReleaseOutputSurface = true
+ }
+ assertThat(isEffectReleased).isFalse()
+ assertThat(requestedToReleaseOutputSurface).isFalse()
+ assertThat(effectSurfaceReadyToRelease).isFalse()
assertThat(appSurfaceReadyToRelease).isFalse()
// effect surface is provided to camera.
- assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effect.inputSurface)
+ assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effectSurface)
// Act: unbind Preview.
preview.onDetached()
shadowOf(getMainLooper()).idle()
// Assert: effect and effect surface is released.
- assertThat(effect.isReleased).isTrue()
- assertThat(effect.isOutputSurfaceRequestedToClose).isTrue()
- assertThat(effect.isInputSurfaceReleased).isTrue()
+ assertThat(isEffectReleased).isTrue()
+ assertThat(requestedToReleaseOutputSurface).isTrue()
+ assertThat(effectSurfaceReadyToRelease).isTrue()
assertThat(appSurfaceReadyToRelease).isFalse()
// Act: close SurfaceOutput
- effect.surfaceOutput!!.close()
+ surfaceOutputReceived!!.close()
shadowOf(getMainLooper()).idle()
assertThat(appSurfaceReadyToRelease).isTrue()
}
@@ -256,8 +282,18 @@
@Test
fun invokedErrorListener_recreatePipeline() {
// Arrange: create pipeline and get a reference of the SessionConfig.
- val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
- val preview = createPreviewPipelineAndAttachEffect(effect)
+ val surfaceEffect = object : SurfaceEffectInternal {
+ override fun onInputSurface(request: SurfaceRequest) {}
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ surfaceOutput.getSurface(mainThreadExecutor()) {
+ surfaceOutput.close()
+ }
+ }
+
+ override fun release() {}
+ }
+ val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
val originalSessionConfig = preview.sessionConfig
// Act: invoke the error listener.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
index 37ea1bf..1896e6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
@@ -26,8 +26,6 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@@ -44,13 +42,6 @@
private static final Integer TAG_VALUE_2 = 2;
TagBundle mTagBundle;
- private static final List<String> KEY_LIST = new ArrayList<>();
-
- static {
- KEY_LIST.add(TAG_0);
- KEY_LIST.add(TAG_1);
- KEY_LIST.add(TAG_2);
- }
@Before
public void setUp() {
@@ -84,4 +75,10 @@
assertThat(keyList).containsExactly(TAG_0, TAG_1, TAG_2);
}
+
+ @Test
+ public void verifyTagBundleToString() {
+ assertThat(mTagBundle.toString()).startsWith("android.hardware.camera2.CaptureRequest"
+ + ".setTag.CX");
+ }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 951f021..42f71acb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -19,10 +19,11 @@
import android.os.Build
import androidx.camera.core.EffectBundle
import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceEffect
import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.processing.SurfaceEffectWithExecutor
-import androidx.camera.testing.fakes.FakeSurfaceEffect
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -42,20 +43,22 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraUseCaseAdapterTest {
- private lateinit var surfaceEffect: FakeSurfaceEffect
+ private lateinit var surfaceEffect: SurfaceEffect
private lateinit var mEffectBundle: EffectBundle
private lateinit var executor: ExecutorService
@Before
fun setUp() {
- surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+ surfaceEffect = object : SurfaceEffect {
+ override fun onInputSurface(request: SurfaceRequest) {}
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+ }
executor = Executors.newSingleThreadExecutor()
mEffectBundle = EffectBundle.Builder(executor).addEffect(PREVIEW, surfaceEffect).build()
}
@After
fun tearDown() {
- surfaceEffect.cleanUp()
executor.shutdown()
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
index 7df85dd..ca79bdea 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
@@ -24,10 +24,11 @@
import android.util.Size
import android.view.Surface
import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -54,9 +55,15 @@
private val CROP_RECT = Rect(0, 0, 600, 400)
}
- private lateinit var surfaceEffectInternal: FakeSurfaceEffectInternal
+ private lateinit var surfaceEffect: SurfaceEffectInternal
+ private var isReleased = false
+ private var surfaceOutputCloseRequested = false
+ private var surfaceOutputReceived: SurfaceOutput? = null
+ private var surfaceReceivedByEffect: Surface? = null
private lateinit var appSurface: Surface
private lateinit var appSurfaceTexture: SurfaceTexture
+ private lateinit var effectSurface: Surface
+ private lateinit var effectSurfaceTexture: SurfaceTexture
private lateinit var node: SurfaceEffectNode
private lateinit var inputEdge: SurfaceEdge
@@ -64,8 +71,30 @@
fun setup() {
appSurfaceTexture = SurfaceTexture(0)
appSurface = Surface(appSurfaceTexture)
- surfaceEffectInternal = FakeSurfaceEffectInternal(mainThreadExecutor())
- node = SurfaceEffectNode(FakeCamera(), surfaceEffectInternal)
+ effectSurfaceTexture = SurfaceTexture(0)
+ effectSurface = Surface(effectSurfaceTexture)
+
+ surfaceEffect = object : SurfaceEffectInternal {
+ override fun onInputSurface(request: SurfaceRequest) {
+ request.provideSurface(effectSurface, mainThreadExecutor()) {
+ effectSurfaceTexture.release()
+ effectSurface.release()
+ }
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ surfaceOutputReceived = surfaceOutput
+ surfaceReceivedByEffect = surfaceOutput.getSurface(mainThreadExecutor()) {
+ surfaceOutput.close()
+ surfaceOutputCloseRequested = true
+ }
+ }
+
+ override fun release() {
+ isReleased = true
+ }
+ }
+ node = SurfaceEffectNode(FakeCamera(), surfaceEffect)
inputEdge = createInputEdge()
}
@@ -73,7 +102,8 @@
fun tearDown() {
appSurfaceTexture.release()
appSurface.release()
- surfaceEffectInternal.release()
+ effectSurfaceTexture.release()
+ effectSurface.release()
node.release()
inputEdge.surfaces[0].close()
shadowOf(getMainLooper()).idle()
@@ -110,8 +140,8 @@
shadowOf(getMainLooper()).idle()
// Assert: effect receives app Surface. CameraX receives effect Surface.
- assertThat(surfaceEffectInternal.outputSurface).isEqualTo(appSurface)
- assertThat(inputSurface.surface.get()).isEqualTo(surfaceEffectInternal.inputSurface)
+ assertThat(surfaceReceivedByEffect).isEqualTo(appSurface)
+ assertThat(inputSurface.surface.get()).isEqualTo(effectSurface)
}
@Test
@@ -126,8 +156,8 @@
shadowOf(getMainLooper()).idle()
// Assert: effect is released and has requested effect to close the SurfaceOutput
- assertThat(surfaceEffectInternal.isReleased).isTrue()
- assertThat(surfaceEffectInternal.isOutputSurfaceRequestedToClose).isTrue()
+ assertThat(isReleased).isTrue()
+ assertThat(surfaceOutputCloseRequested).isTrue()
}
private fun createInputEdge(): SurfaceEdge {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
index 6a33afe..86b492e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
@@ -27,7 +27,6 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
import com.google.common.truth.Truth.assertThat
import java.lang.Thread.currentThread
import java.util.concurrent.Executor
@@ -70,10 +69,13 @@
@Test(expected = IllegalStateException::class)
fun initWithSurfaceEffectInternal_throwsException() {
- SurfaceEffectWithExecutor(
- FakeSurfaceEffectInternal(mainThreadExecutor()),
- mainThreadExecutor()
- )
+ SurfaceEffectWithExecutor(object : SurfaceEffectInternal {
+ override fun onInputSurface(request: SurfaceRequest) {}
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+
+ override fun release() {}
+ }, mainThreadExecutor())
}
@Test
diff --git a/camera/camera-extensions/lint-baseline.xml b/camera/camera-extensions/lint-baseline.xml
index 382b9d2..eb45276 100644
--- a/camera/camera-extensions/lint-baseline.xml
+++ b/camera/camera-extensions/lint-baseline.xml
@@ -4,51 +4,6 @@
<issue
id="UnsafeOptInUsageError"
message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" captureParameter.second);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" captureConfigBuilder.addImplementationOptions(camera2ConfigurationBuilder.build());"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
errorLine1=" CaptureRequestOptions.Builder.from(parameters).build();"
errorLine2=" ~~~~">
<location
@@ -124,42 +79,6 @@
errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new CameraEventCallbacks(imageCaptureEventAdapter));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
file="src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java"/>
</issue>
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
index 225da9a..f9bc984 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
@@ -20,8 +20,10 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.CaptureStage;
import androidx.camera.extensions.impl.CaptureStageImpl;
@@ -34,9 +36,9 @@
private final int mId;
@SuppressWarnings("unchecked")
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
public AdaptingCaptureStage(@NonNull CaptureStageImpl impl) {
mId = impl.getId();
-
Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();
for (Pair<CaptureRequest.Key, Object> captureParameter : impl.getParameters()) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index c280363..c752065 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -88,6 +88,7 @@
/**
* Update extension related configs to the builder.
*/
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
void updateBuilderConfig(@NonNull ImageCapture.Builder builder,
@ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
@NonNull Context context) {
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index 35e8e77..bf87a7f 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,7 +26,10 @@
import androidx.camera.core.CameraXConfig
import androidx.camera.core.EffectBundle
import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceEffect
import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
@@ -37,7 +40,6 @@
import androidx.camera.testing.fakes.FakeCameraFactory
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSurfaceEffect
import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
import androidx.concurrent.futures.await
import androidx.test.core.app.ApplicationProvider
@@ -79,7 +81,12 @@
fun bindUseCaseGroupWithEffect_effectIsSetOnUseCase() {
// Arrange.
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
- val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+ val surfaceEffect = object : SurfaceEffect {
+ override fun onInputSurface(request: SurfaceRequest) {}
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ surfaceOutput.close()
+ }
+ }
val effectBundle =
EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
val preview = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
deleted file mode 100644
index 1287dee..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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.camera.testing.fakes;
-
-import android.graphics.SurfaceTexture;
-import android.os.Build;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.SurfaceEffect;
-import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceRequest;
-import androidx.camera.core.impl.DeferrableSurface;
-
-import java.util.concurrent.Executor;
-
-/**
- * Fake {@link SurfaceEffect} used in tests.
- */
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class FakeSurfaceEffect implements SurfaceEffect {
-
- final SurfaceTexture mSurfaceTexture;
- final Surface mInputSurface;
- private final Executor mExecutor;
-
-
- @Nullable
- private SurfaceRequest mSurfaceRequest;
- @Nullable
- private SurfaceOutput mSurfaceOutput;
- boolean mIsInputSurfaceReleased;
- boolean mIsOutputSurfaceRequestedToClose;
-
- Surface mOutputSurface;
-
- public FakeSurfaceEffect(@NonNull Executor executor) {
- mSurfaceTexture = new SurfaceTexture(0);
- mInputSurface = new Surface(mSurfaceTexture);
- mExecutor = executor;
- mIsInputSurfaceReleased = false;
- mIsOutputSurfaceRequestedToClose = false;
- }
-
- @Override
- public void onInputSurface(@NonNull SurfaceRequest request) {
- mSurfaceRequest = request;
- request.provideSurface(mInputSurface, mExecutor, result -> {
- mSurfaceTexture.release();
- mInputSurface.release();
- mIsInputSurfaceReleased = true;
- });
- }
-
- @Override
- public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
- mSurfaceOutput = surfaceOutput;
- mOutputSurface = surfaceOutput.getSurface(mExecutor,
- () -> mIsOutputSurfaceRequestedToClose = true);
- }
-
- @Nullable
- public SurfaceRequest getSurfaceRequest() {
- return mSurfaceRequest;
- }
-
- @Nullable
- public SurfaceOutput getSurfaceOutput() {
- return mSurfaceOutput;
- }
-
- @NonNull
- public Surface getInputSurface() {
- return mInputSurface;
- }
-
- @NonNull
- public Surface getOutputSurface() {
- return mOutputSurface;
- }
-
- public boolean isInputSurfaceReleased() {
- return mIsInputSurfaceReleased;
- }
-
- public boolean isOutputSurfaceRequestedToClose() {
- return mIsOutputSurfaceRequestedToClose;
- }
-
- /**
- * Clear up the instance to avoid the "{@link DeferrableSurface} garbage collected" error.
- */
- public void cleanUp() {
- if (mSurfaceRequest != null) {
- mSurfaceRequest.willNotProvideSurface();
- }
- if (mSurfaceOutput != null) {
- mSurfaceOutput.close();
- }
- mSurfaceTexture.release();
- mInputSurface.release();
- }
-
- @Override
- protected void finalize() {
- cleanUp();
- }
-}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
deleted file mode 100644
index 47bd6f7..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.camera.testing.fakes;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.processing.SurfaceEffectInternal;
-
-import java.util.concurrent.Executor;
-
-/**
- * Fake {@link SurfaceEffectInternal} used in tests.
- */
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class FakeSurfaceEffectInternal extends FakeSurfaceEffect implements SurfaceEffectInternal {
-
- private boolean mIsReleased;
-
- public FakeSurfaceEffectInternal(@NonNull Executor executor) {
- super(executor);
- mIsReleased = false;
- }
-
- public boolean isReleased() {
- return mIsReleased;
- }
-
- @Override
- public void release() {
- mIsReleased = true;
- }
-}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
index 2e5c7b8..c478811 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
@@ -25,7 +25,10 @@
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.EffectBundle
import androidx.camera.core.ImageCapture
+import androidx.camera.core.SurfaceEffect
import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
@@ -33,7 +36,6 @@
import androidx.camera.testing.CoreAppTestUtil
import androidx.camera.testing.fakes.FakeActivity
import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSurfaceEffect
import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
@@ -103,10 +105,15 @@
// Act: set an EffectBundle
instrumentation.runOnMainSync {
+ val surfaceEffect = object : SurfaceEffect {
+ override fun onInputSurface(request: SurfaceRequest) {}
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ surfaceOutput.close()
+ }
+ }
controller.setEffectBundle(
- EffectBundle.Builder(mainThreadExecutor())
- .addEffect(PREVIEW, FakeSurfaceEffect(mainThreadExecutor()))
- .build()
+ EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
)
}
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
index 74a5c35..6f91eca 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
@@ -16,7 +16,9 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioTrack
+import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
@@ -33,6 +35,7 @@
@RunWith(AndroidJUnit4::class)
class AudioGeneratorDeviceTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var audioGenerator: AudioGenerator
@Before
@@ -42,12 +45,12 @@
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenFrequencyNegative() = runTest {
- audioGenerator.initAudioTrack(-5300, 11.0)
+ audioGenerator.initAudioTrack(context, -5300, 11.0)
}
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenLengthNegative() = runTest {
- audioGenerator.initAudioTrack(5300, -11.0)
+ audioGenerator.initAudioTrack(context, 5300, -11.0)
}
@Test
@@ -71,7 +74,7 @@
}
private suspend fun initialAudioTrack(frequency: Int, beepLengthInSec: Double) {
- val isInitialized = audioGenerator.initAudioTrack(frequency, beepLengthInSec)
+ val isInitialized = audioGenerator.initAudioTrack(context, frequency, beepLengthInSec)
assertThat(isInitialized).isTrue()
assertThat(audioGenerator.audioTrack!!.state).isEqualTo(AudioTrack.STATE_INITIALIZED)
assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isEqualTo(0)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
index 2997a17..2762f2d 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -79,6 +79,7 @@
withContext(Dispatchers.Default) {
audioGenerator.initAudioTrack(
+ context = context,
frequency = beepFrequency,
beepLengthInSec = ACTIVE_LENGTH_SEC,
)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
index 80c3936..8e12eb2 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
@@ -30,8 +31,8 @@
import kotlin.math.sin
private const val TAG = "AudioGenerator"
+private const val DEFAULT_SAMPLE_RATE: Int = 44100
private const val SAMPLE_WIDTH: Int = 2
-private const val SAMPLE_RATE: Int = 44100
private const val MAGNITUDE = 0.5
private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT
private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO
@@ -46,26 +47,33 @@
}
fun stop() {
+ Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}")
+ Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}")
audioTrack!!.stop()
}
suspend fun initAudioTrack(
+ context: Context,
frequency: Int,
beepLengthInSec: Double,
): Boolean {
checkArgumentNonnegative(frequency, "The input frequency should not be negative.")
checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.")
- Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
-
- val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, SAMPLE_RATE)
+ val sampleRate = getOutputSampleRate(context)
+ val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, sampleRate)
val bufferSize = samples.size
+
+ Logger.i(TAG, "initAudioTrack with sample rate: $sampleRate")
+ Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
+ Logger.i(TAG, "initAudioTrack with buffer size: $bufferSize")
+
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val audioFormat = AudioFormat.Builder()
- .setSampleRate(SAMPLE_RATE)
+ .setSampleRate(sampleRate)
.setEncoding(ENCODING)
.setChannelMask(CHANNEL)
.build()
@@ -83,6 +91,13 @@
return true
}
+ private fun getOutputSampleRate(context: Context): Int {
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val sampleRate: String? = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
+
+ return sampleRate?.toInt() ?: DEFAULT_SAMPLE_RATE
+ }
+
@VisibleForTesting
suspend fun generateSineSamples(
frequency: Int,
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 4bab0a9..cb47a1d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -31,8 +31,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.waitForViewfinderIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(preview).isNotNull()
resetViewIdlingResource()
viewIdlingResource
}
@@ -50,8 +48,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.switchCameraAndWaitForViewfinderIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(preview).isNotNull()
resetViewIdlingResource()
viewIdlingResource
}
@@ -68,8 +64,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(imageCapture).isNotNull()
imageSavedIdlingResource
}
try {
@@ -88,8 +82,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.waitForImageAnalysisIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(imageAnalysis).isNotNull()
resetAnalysisIdlingResource()
analysisIdlingResource
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 0636c9b..981b5d2 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,10 +18,13 @@
import android.Manifest
import android.app.Instrumentation
import android.content.Context
+import android.content.Intent
import android.os.Build
import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
@@ -29,10 +32,9 @@
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
@@ -42,20 +44,23 @@
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.junit.After
-import org.junit.AfterClass
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
private const val HOME_TIMEOUT_MS = 3000L
private const val ROTATE_TIMEOUT_MS = 2000L
// Test application lifecycle when using CameraX.
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
@LargeTest
-class ExistingActivityLifecycleTest {
+class ExistingActivityLifecycleTest(
+ private val implName: String,
+ private val cameraConfig: String
+) {
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
@@ -73,14 +78,17 @@
@get:Rule
val repeatRule = RepeatRule()
- companion object {
- @AfterClass
- @JvmStatic
- fun shutdownCameraX() {
- val context = ApplicationProvider.getApplicationContext<Context>()
- val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
- cameraProvider.shutdown()[10, TimeUnit.SECONDS]
- }
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ forAllTests = true,
+ )
+
+ private val launchIntent = Intent(
+ ApplicationProvider.getApplicationContext(),
+ CameraXActivity::class.java
+ ).apply {
+ putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
}
@Before
@@ -108,13 +116,17 @@
device.unfreezeRotation()
device.pressHome()
device.waitForIdle(HOME_TIMEOUT_MS)
+
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
// Check if Preview screen is updated or not, after Destroy-Create lifecycle.
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -129,7 +141,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -150,7 +162,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -179,7 +191,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -210,13 +222,13 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
// Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Check front camera is now idle
@@ -244,7 +256,7 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -252,7 +264,7 @@
waitForViewfinderIdle()
// Act. Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Assert.
@@ -272,7 +284,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -298,7 +310,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -337,4 +349,20 @@
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
+
+ companion object {
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(
+ Camera2Config::class.simpleName,
+ CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+ ),
+ arrayOf(
+ CameraPipeConfig::class.simpleName,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+ )
+ )
+ }
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
deleted file mode 100644
index 7951333..0000000
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright 2019 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.camera.integration.core;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-
-import static junit.framework.TestCase.assertNotNull;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Intent;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.TorchState;
-import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
-import androidx.camera.integration.core.idlingresource.WaitForViewToShow;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.CoreAppTestUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.IdlingResource;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.GrantPermissionRule;
-import androidx.test.uiautomator.UiDevice;
-
-import junit.framework.AssertionFailedError;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Test toggle buttons in CoreTestApp. */
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public final class ToggleButtonUITest {
-
- private static final int IDLE_TIMEOUT_MS = 1000;
- private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
-
- private final UiDevice mDevice =
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- private final Intent mIntent = ApplicationProvider.getApplicationContext().getPackageManager()
- .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
-
- @Rule
- public ActivityTestRule<CameraXActivity> mActivityRule =
- new ActivityTestRule<>(CameraXActivity.class, true,
- false);
-
- @Rule
- public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
- new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
- );
- @Rule
- public GrantPermissionRule mStoragePermissionRule =
- GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
- @Rule
- public GrantPermissionRule mAudioPermissionRule =
- GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
-
- public static void waitFor(IdlingResource idlingResource) {
- IdlingRegistry.getInstance().register(idlingResource);
- Espresso.onIdle();
- IdlingRegistry.getInstance().unregister(idlingResource);
- }
-
- @Before
- public void setUp() throws CoreAppTestUtil.ForegroundOccupiedError {
- assumeTrue(CameraUtil.deviceHasCamera());
- CoreAppTestUtil.assumeCompatibleDevice();
-
- // Clear the device UI and check if there is no dialog or lock screen on the top of the
- // window before start the test.
- CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation());
-
- // Launch Activity
- mActivityRule.launchActivity(mIntent);
- }
-
- @After
- public void tearDown() {
- // Idles Espresso thread and make activity complete each action.
- waitFor(new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS));
-
- mActivityRule.finishActivity();
-
- // Returns to Home to restart next test.
- mDevice.pressHome();
- mDevice.waitForIdle(IDLE_TIMEOUT_MS);
- }
-
- @AfterClass
- public static void shutdownCameraX()
- throws InterruptedException, ExecutionException, TimeoutException {
- ProcessCameraProvider cameraProvider = ProcessCameraProvider.getInstance(
- ApplicationProvider.getApplicationContext()).get(10, TimeUnit.SECONDS);
- cameraProvider.shutdown().get(10, TimeUnit.SECONDS);
- }
-
-
- @Test
- public void testFlashToggleButton() {
- waitFor(new WaitForViewToShow(R.id.constraintLayout));
- assumeTrue(isButtonEnabled(R.id.flash_toggle));
-
- ImageCapture useCase = mActivityRule.getActivity().getImageCapture();
- assertNotNull(useCase);
-
- // There are 3 different states of flash mode: ON, OFF and AUTO.
- // By pressing flash mode toggle button, the flash mode would switch to the next state.
- // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
- @ImageCapture.FlashMode int mode1 = useCase.getFlashMode();
-
- onView(withId(R.id.flash_toggle)).perform(click());
- @ImageCapture.FlashMode int mode2 = useCase.getFlashMode();
- // After the switch, the mode2 should be different from mode1.
- assertNotEquals(mode2, mode1);
-
- onView(withId(R.id.flash_toggle)).perform(click());
- @ImageCapture.FlashMode int mode3 = useCase.getFlashMode();
- // The mode3 should be different from first and second time.
- assertNotEquals(mode3, mode2);
- assertNotEquals(mode3, mode1);
- }
-
- @Test
- public void testTorchToggleButton() {
- waitFor(new WaitForViewToShow(R.id.constraintLayout));
- assumeTrue(isButtonEnabled(R.id.torch_toggle));
-
- CameraInfo cameraInfo = mActivityRule.getActivity().getCameraInfo();
- assertNotNull(cameraInfo);
- boolean isTorchOn = isTorchOn(cameraInfo);
-
- onView(withId(R.id.torch_toggle)).perform(click());
- assertNotEquals(isTorchOn(cameraInfo), isTorchOn);
-
- // By pressing the torch toggle button two times, it should switch back to original state.
- onView(withId(R.id.torch_toggle)).perform(click());
- assertEquals(isTorchOn(cameraInfo), isTorchOn);
- }
-
- @Test
- public void testSwitchCameraToggleButton() {
- assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT));
- waitFor(new WaitForViewToShow(R.id.direction_toggle));
-
- assumeNotNull(mActivityRule.getActivity().getPreview());
-
- for (int i = 0; i < 5; i++) {
-
- // Wait for preview update.
- mActivityRule.getActivity().resetViewIdlingResource();
- IdlingRegistry.getInstance().register(
- mActivityRule.getActivity().getViewIdlingResource());
- onView(withId(R.id.viewFinder)).check(matches(isDisplayed()));
- IdlingRegistry.getInstance().unregister(
- mActivityRule.getActivity().getViewIdlingResource());
-
- onView(withId(R.id.direction_toggle)).perform(click());
- }
- }
-
- private boolean isTorchOn(CameraInfo cameraInfo) {
- return cameraInfo.getTorchState().getValue() == TorchState.ON;
- }
-
- private boolean isButtonEnabled(int resource) {
- try {
- onView(withId(resource)).check(matches(isEnabled()));
- // View is in hierarchy
- return true;
- } catch (AssertionFailedError e) {
- // View is not in hierarchy
- return false;
- } catch (Exception e) {
- // View is not in hierarchy
- return false;
- }
- }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
new file mode 100644
index 0000000..d7fc670
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2021 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.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.integration.core.idlingresource.WaitForViewToShow
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onIdle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import junit.framework.AssertionFailedError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/** Test toggle buttons in CoreTestApp. */
+@LargeTest
+@RunWith(Parameterized::class)
+class ToggleButtonUITest(
+ private val implName: String,
+ private val cameraConfig: String
+) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(
+ if (implName == Camera2Config::class.simpleName) {
+ Camera2Config.defaultConfig()
+ } else {
+ CameraPipeConfig.defaultConfig()
+ }
+ )
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ forAllTests = true,
+ )
+
+ private val launchIntent = Intent(
+ ApplicationProvider.getApplicationContext(),
+ CameraXActivity::class.java
+ ).apply {
+ putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(CameraUtil.deviceHasCamera())
+ CoreAppTestUtil.assumeCompatibleDevice()
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
+ // Clear the device UI and check if there is no dialog or lock screen on the top of the
+ // window before start the test.
+ CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ }
+
+ @After
+ fun tearDown(): Unit = runBlocking(Dispatchers.Main) {
+ // Returns to Home to restart next test.
+ device.pressHome()
+ device.waitForIdle(IDLE_TIMEOUT_MS)
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+ }
+
+ @Test
+ fun testFlashToggleButton() {
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ // Arrange.
+ WaitForViewToShow(R.id.constraintLayout).wait()
+ assumeTrue(isButtonEnabled(R.id.flash_toggle))
+ val useCase = scenario.withActivity { imageCapture }
+ // There are 3 different states of flash mode: ON, OFF and AUTO.
+ // By pressing flash mode toggle button, the flash mode would switch to the next state.
+ // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
+ // Act.
+ @ImageCapture.FlashMode val mode1 = useCase.flashMode
+ onView(withId(R.id.flash_toggle)).perform(click())
+ @ImageCapture.FlashMode val mode2 = useCase.flashMode
+ onView(withId(R.id.flash_toggle)).perform(click())
+ @ImageCapture.FlashMode val mode3 = useCase.flashMode
+
+ // Assert.
+ // After the switch, the mode2 should be different from mode1.
+ assertThat(mode2).isNotEqualTo(mode1)
+ // The mode3 should be different from first and second time.
+ assertThat(mode3).isNoneOf(mode2, mode1)
+ }
+ }
+
+ @Test
+ fun testTorchToggleButton() {
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ WaitForViewToShow(R.id.constraintLayout).wait()
+ assumeTrue(isButtonEnabled(R.id.torch_toggle))
+ val cameraInfo = scenario.withActivity { cameraInfo!! }
+ val isTorchOn = cameraInfo.isTorchOn()
+ onView(withId(R.id.torch_toggle)).perform(click())
+ assertThat(cameraInfo.isTorchOn()).isNotEqualTo(isTorchOn)
+ // By pressing the torch toggle button two times, it should switch back to original
+ // state.
+ onView(withId(R.id.torch_toggle)).perform(click())
+ assertThat(cameraInfo.isTorchOn()).isEqualTo(isTorchOn)
+ }
+ }
+
+ @Test
+ fun testSwitchCameraToggleButton() {
+ assumeTrue(
+ "Ignore the camera switch test since there's no front camera.",
+ CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)
+ )
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ WaitForViewToShow(R.id.direction_toggle).wait()
+ assertThat(scenario.withActivity { preview }).isNotNull()
+ for (i in 0..4) {
+ scenario.waitForViewfinderIdle()
+ // Click switch camera button.
+ onView(withId(R.id.direction_toggle)).perform(click())
+ }
+ }
+ }
+
+ private fun CameraInfo.isTorchOn(): Boolean = torchState.value == TorchState.ON
+
+ private fun isButtonEnabled(resource: Int): Boolean {
+ return try {
+ onView(withId(resource))
+ .check(ViewAssertions.matches(ViewMatchers.isEnabled()))
+ // View is in hierarchy
+ true
+ } catch (e: AssertionFailedError) {
+ // View is not in hierarchy
+ false
+ } catch (e: Exception) {
+ // View is not in hierarchy
+ false
+ }
+ }
+
+ private fun IdlingResource.wait() {
+ IdlingRegistry.getInstance().register(this)
+ onIdle()
+ IdlingRegistry.getInstance().unregister(this)
+ }
+
+ companion object {
+ private const val IDLE_TIMEOUT_MS = 1_000L
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(
+ Camera2Config::class.simpleName,
+ CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+ ),
+ arrayOf(
+ CameraPipeConfig::class.simpleName,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+ )
+ )
+ }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
similarity index 93%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
rename to camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index a351980..f61bdc6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.camera.camera2
+package androidx.camera.integration.core.camera2
import android.content.Context
import android.graphics.SurfaceTexture
import android.util.Size
import android.view.Surface
+import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCase
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -37,7 +39,6 @@
import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -50,24 +51,40 @@
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
+import org.junit.runners.Parameterized
@LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
@SdkSuppress(minSdkVersion = 21)
-class PreviewTest {
+class PreviewTest(
+ private val implName: String,
+ private val cameraConfig: CameraXConfig
+) {
@get:Rule
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
+ PreTestCameraIdList(cameraConfig)
)
+
+ companion object {
+ private const val ANY_THREAD_NAME = "any-thread-name"
+ private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var defaultBuilder: Preview.Builder? = null
@@ -81,8 +98,7 @@
@Throws(ExecutionException::class, InterruptedException::class)
fun setUp() {
context = ApplicationProvider.getApplicationContext()
- val cameraXConfig = Camera2Config.defaultConfig()
- CameraXUtil.initialize(context!!, cameraXConfig).get()
+ CameraXUtil.initialize(context!!, cameraConfig).get()
// init CameraX before creating Preview to get preview size with CameraX's context
defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
@@ -106,38 +122,30 @@
}
@Test
- fun surfaceProvider_isUsedAfterSetting() {
- val surfaceProvider = Mockito.mock(
- Preview.SurfaceProvider::class.java
- )
- Mockito.doAnswer { args: InvocationOnMock ->
- val surfaceTexture = SurfaceTexture(0)
- surfaceTexture.setDefaultBufferSize(640, 480)
- val surface = Surface(surfaceTexture)
- (args.getArgument<Any>(0) as SurfaceRequest).provideSurface(
- surface,
- CameraXExecutors.directExecutor()
- ) {
- surfaceTexture.release()
- surface.release()
- }
- null
- }.`when`(surfaceProvider).onSurfaceRequested(
- ArgumentMatchers.any(
- SurfaceRequest::class.java
- )
- )
+ fun surfaceProvider_isUsedAfterSetting() = runBlocking {
val preview = defaultBuilder!!.build()
+ val completableDeferred = CompletableDeferred<Unit>()
// TODO(b/160261462) move off of main thread when setSurfaceProvider does not need to be
// done on the main thread
- instrumentation.runOnMainSync { preview.setSurfaceProvider(surfaceProvider) }
- camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
- Mockito.verify(surfaceProvider, Mockito.timeout(3000)).onSurfaceRequested(
- ArgumentMatchers.any(
- SurfaceRequest::class.java
+ instrumentation.runOnMainSync { preview.setSurfaceProvider { request ->
+ val surfaceTexture = SurfaceTexture(0)
+ surfaceTexture.setDefaultBufferSize(
+ request.resolution.width,
+ request.resolution.height
)
- )
+ surfaceTexture.detachFromGLContext()
+ val surface = Surface(surfaceTexture)
+ request.provideSurface(surface, CameraXExecutors.directExecutor()) {
+ surface.release()
+ surfaceTexture.release()
+ }
+ completableDeferred.complete(Unit)
+ } }
+ camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
+ withTimeout(3_000) {
+ completableDeferred.await()
+ }
}
@Test
@@ -564,9 +572,4 @@
} while (totalCheckTime < timeoutMs)
return false
}
-
- companion object {
- private const val ANY_THREAD_NAME = "any-thread-name"
- private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
- }
}
\ No newline at end of file
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
index 38b9612..0eb3827 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.os.Build
import android.util.Log
-import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
@@ -30,8 +29,8 @@
*/
class Diagnosis {
- // TODO: convert to async function
- fun collectDeviceInfo(context: Context) {
+ // TODO: convert to a suspend function for running different tasks within this function
+ fun collectDeviceInfo(context: Context): File {
Log.d(TAG, "calling collectDeviceInfo()")
// TODO: verify if external storage is available
@@ -57,12 +56,7 @@
zout.close()
fout.close()
- Log.d(TAG, "file at ${tempFile.path}")
- if (tempFile.exists()) {
- val msg = "Successfully collected information"
- Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
- Log.d(TAG, msg)
- }
+ return tempFile
}
private fun createTemp(context: Context, filename: String): File {
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index 519dad6..f195557 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,7 +32,6 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.view.CameraController
import androidx.camera.view.CameraController.IMAGE_CAPTURE
import androidx.camera.view.CameraController.VIDEO_CAPTURE
@@ -46,13 +45,21 @@
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Locale
-import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayout
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScanning
import java.io.IOException
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import kotlinx.coroutines.ExecutorCoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
@@ -65,6 +72,9 @@
private lateinit var barcodeScanner: BarcodeScanner
private lateinit var analyzer: MlKitAnalyzer
private lateinit var diagnoseBtn: Button
+ private lateinit var calibrationExecutor: ExecutorService
+ private var calibrationThreadId: Long = -1
+ private lateinit var diagnosisDispatcher: ExecutorCoroutineDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,6 +89,13 @@
diagnosis = Diagnosis()
barcodeScanner = BarcodeScanning.getClient()
diagnoseBtn = findViewById(R.id.diagnose_btn)
+ calibrationExecutor = Executors.newSingleThreadExecutor() { runnable ->
+ val thread = Executors.defaultThreadFactory().newThread(runnable)
+ thread.name = "CalibrationThread"
+ calibrationThreadId = thread.id
+ return@newSingleThreadExecutor thread
+ }
+ diagnosisDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
// Request CAMERA permission and fail gracefully if not granted.
if (allPermissionsGranted()) {
@@ -125,12 +142,20 @@
})
diagnoseBtn.setOnClickListener {
- try {
- diagnosis.collectDeviceInfo(baseContext)
- } catch (e: IOException) {
- val msg = "Failed to collect information"
- Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
- Log.e(TAG, "IOException caught: ${e.message}")
+ lifecycleScope.launch {
+ try {
+ val tempFile = withContext(diagnosisDispatcher) {
+ Log.i(TAG, "dispatcher: ${Thread.currentThread().name}")
+ diagnosis.collectDeviceInfo(baseContext)
+ }
+ Log.d(TAG, "file at ${tempFile.path}")
+ val msg = "Successfully collected device info"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ } catch (e: IOException) {
+ val msg = "Failed to collect information"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ Log.e(TAG, "IOException caught: ${e.message}")
+ }
}
}
}
@@ -287,20 +312,25 @@
analyzer = MlKitAnalyzer(
listOf(barcodeScanner),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
- CameraXExecutors.mainThreadExecutor()
+ calibrationExecutor
) { result ->
+ // validating thread
+ checkCalibrationThread()
val barcodes = result.getValue(barcodeScanner)
if (barcodes != null && barcodes.size > 0) {
calibrate.analyze(barcodes)
- // gives overlayView access to Calibration
- overlayView.setCalibrationResult(calibrate)
- // enable diagnose button when alignment is successful
- diagnoseBtn.isEnabled = calibrate.isAligned
- overlayView.invalidate()
+ // run UI on main thread
+ lifecycleScope.launch {
+ // gives overlayView access to Calibration
+ overlayView.setCalibrationResult(calibrate)
+ // enable diagnose button when alignment is successful
+ diagnoseBtn.isEnabled = calibrate.isAligned
+ overlayView.invalidate()
+ }
}
}
cameraController.setImageAnalysisAnalyzer(
- CameraXExecutors.mainThreadExecutor(), analyzer)
+ calibrationExecutor, analyzer)
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -309,6 +339,17 @@
) == PackageManager.PERMISSION_GRANTED
}
+ private fun checkCalibrationThread() {
+ Preconditions.checkState(calibrationThreadId == Thread.currentThread().id,
+ "Not working on Calibration Thread")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ calibrationExecutor.shutdown()
+ diagnosisDispatcher.close()
+ }
+
companion object {
private const val TAG = "DiagnoseApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 5aa38fc..48ca50a 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
<activity
android:name=".CameraExtensionsActivity"
android:exported="true"
- android:label="Camera Extensions">
+ android:label="CameraX Extensions">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -35,6 +35,16 @@
</activity>
<activity
+ android:name=".Camera2ExtensionsActivity"
+ android:exported="false"
+ android:label="Camera2 Extensions">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".validation.CameraValidationResultActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="false">
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
new file mode 100644
index 0000000..d682e82
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -0,0 +1,661 @@
+/*
+ * 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.camera.integration.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.ExtensionSessionConfiguration
+import android.hardware.camera2.params.OutputConfiguration
+import android.media.ImageReader
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.Surface
+import android.view.TextureView
+import android.view.ViewStub
+import android.widget.Button
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.calculateRelativeImageRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.createExtensionCaptureCallback
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getDisplayRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getLensFacingCameraId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickPreviewResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickStillImageResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.transformPreview
+import androidx.camera.integration.extensions.utils.FileUtil
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
+import com.google.common.util.concurrent.ListenableFuture
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.concurrent.Executors
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "Camera2ExtensionsAct~"
+private const val EXTENSION_MODE_INVALID = -1
+
+@RequiresApi(31)
+class Camera2ExtensionsActivity : AppCompatActivity() {
+
+ private lateinit var cameraManager: CameraManager
+
+ /**
+ * A reference to the opened [CameraDevice].
+ */
+ private var cameraDevice: CameraDevice? = null
+
+ /**
+ * The current camera extension session.
+ */
+ private var cameraExtensionSession: CameraExtensionSession? = null
+
+ private var currentCameraId = "0"
+
+ private lateinit var backCameraId: String
+ private lateinit var frontCameraId: String
+
+ private var cameraSensorRotationDegrees = 0
+
+ /**
+ * Still capture image reader
+ */
+ private var stillImageReader: ImageReader? = null
+
+ /**
+ * Camera extension characteristics for the current camera device.
+ */
+ private lateinit var extensionCharacteristics: CameraExtensionCharacteristics
+
+ /**
+ * Flag whether we should restart preview after an extension switch.
+ */
+ private var restartPreview = false
+
+ /**
+ * Flag whether we should restart after an camera switch.
+ */
+ private var restartCamera = false
+
+ /**
+ * Track current extension type and index.
+ */
+ private var currentExtensionMode = EXTENSION_MODE_INVALID
+ private var currentExtensionIdx = -1
+ private val supportedExtensionModes = mutableListOf<Int>()
+
+ private lateinit var textureView: TextureView
+
+ private lateinit var previewSurface: Surface
+
+ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ previewSurface = Surface(surfaceTexture)
+ openCameraWithExtensionMode(currentCameraId)
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ }
+
+ override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
+ }
+ }
+
+ private val captureCallbacks = createExtensionCaptureCallback()
+
+ private var restartOnStart = false
+
+ private var activityStopped = false
+
+ private val cameraTaskDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+ private var imageSaveTerminationFuture: ListenableFuture<Any?> = Futures.immediateFuture(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "onCreate()")
+ setContentView(R.layout.activity_camera_extensions)
+
+ cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ backCameraId = getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_BACK)
+ frontCameraId =
+ getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_FRONT)
+
+ currentCameraId = if (isCameraSupportExtensions(backCameraId)) {
+ backCameraId
+ } else if (isCameraSupportExtensions(frontCameraId)) {
+ frontCameraId
+ } else {
+ Toast.makeText(
+ this,
+ "Can't find camera supporting Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return
+ }
+
+ updateExtensionInfo()
+
+ setupTextureView()
+ enableUiControl(false)
+ setupUiControl()
+ }
+
+ private fun isCameraSupportExtensions(cameraId: String): Boolean {
+ val characteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ return characteristics.supportedExtensions.isNotEmpty()
+ }
+
+ private fun updateExtensionInfo() {
+ Log.d(
+ TAG,
+ "updateExtensionInfo() - camera Id: $currentCameraId, ${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+ )
+ extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(currentCameraId)
+ supportedExtensionModes.clear()
+ supportedExtensionModes.addAll(extensionCharacteristics.supportedExtensions)
+
+ cameraSensorRotationDegrees = cameraManager.getCameraCharacteristics(
+ currentCameraId)[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0
+
+ currentExtensionIdx = -1
+
+ // Checks whether the original selected extension mode is supported by the new target camera
+ if (currentExtensionMode != EXTENSION_MODE_INVALID) {
+ for (i in 0..supportedExtensionModes.size) {
+ if (supportedExtensionModes[i] == currentExtensionMode) {
+ currentExtensionIdx = i
+ break
+ }
+ }
+ }
+
+ // Switches to the first supported extension mode if the original selected mode is not
+ // supported
+ if (currentExtensionIdx == -1) {
+ currentExtensionIdx = 0
+ currentExtensionMode = supportedExtensionModes[0]
+ }
+ }
+
+ private fun setupTextureView() {
+ val viewFinderStub = findViewById<ViewStub>(R.id.viewFinderStub)
+ viewFinderStub.layoutResource = R.layout.full_textureview
+ textureView = viewFinderStub.inflate() as TextureView
+ textureView.surfaceTextureListener = surfaceTextureListener
+ }
+
+ private fun enableUiControl(enabled: Boolean) {
+ findViewById<Button>(R.id.PhotoToggle).isEnabled = enabled
+ findViewById<Button>(R.id.Switch).isEnabled = enabled
+ findViewById<Button>(R.id.Picture).isEnabled = enabled
+ }
+
+ private fun setupUiControl() {
+ val extensionModeToggleButton = findViewById<Button>(R.id.PhotoToggle)
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+ extensionModeToggleButton.setOnClickListener {
+ enableUiControl(false)
+ currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensionModes.size
+ currentExtensionMode = supportedExtensionModes[currentExtensionIdx]
+ restartPreview = true
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+
+ closeCaptureSession()
+ }
+
+ val cameraSwitchButton = findViewById<Button>(R.id.Switch)
+ cameraSwitchButton.setOnClickListener {
+ val newCameraId = if (currentCameraId == backCameraId) frontCameraId else backCameraId
+
+ if (!isCameraSupportExtensions(newCameraId)) {
+ Toast.makeText(
+ this,
+ "Camera of the other lens facing doesn't support Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@setOnClickListener
+ }
+
+ enableUiControl(false)
+ currentCameraId = newCameraId
+ restartCamera = true
+
+ closeCamera()
+ }
+
+ val captureButton = findViewById<Button>(R.id.Picture)
+ captureButton.setOnClickListener {
+ enableUiControl(false)
+ takePicture()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Log.d(TAG, "onStart()")
+ activityStopped = false
+ if (restartOnStart) {
+ restartOnStart = false
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+
+ override fun onStop() {
+ Log.d(TAG, "onStop()++")
+ super.onStop()
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+ restartOnStart = true
+ activityStopped = true
+ Log.d(TAG, "onStop()--")
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy()++")
+ super.onDestroy()
+ previewSurface.release()
+
+ imageSaveTerminationFuture.addListener({ stillImageReader?.close() }, mainExecutor)
+ Log.d(TAG, "onDestroy()--")
+ }
+
+ private fun closeCamera(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCamera()++")
+ cameraDevice?.close()
+ cameraDevice = null
+ Log.d(TAG, "closeCamera()--")
+ }
+
+ private fun closeCaptureSession(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCaptureSession()++")
+ try {
+ cameraExtensionSession?.close()
+ cameraExtensionSession = null
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ Log.d(TAG, "closeCaptureSession()--")
+ }
+
+ private fun openCameraWithExtensionMode(cameraId: String) =
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ Log.d(TAG, "openCameraWithExtensionMode()++ cameraId: $cameraId")
+ cameraDevice = openCamera(cameraManager, cameraId)
+ cameraExtensionSession = openCaptureSession()
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (activityStopped) {
+ closeCaptureSession()
+ closeCamera()
+ }
+ }
+ Log.d(TAG, "openCameraWithExtensionMode()--")
+ }
+
+ /**
+ * Opens and returns the camera (as the result of the suspend coroutine)
+ */
+ @SuppressLint("MissingPermission")
+ suspend fun openCamera(
+ manager: CameraManager,
+ cameraId: String,
+ ): CameraDevice = suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCamera(): $cameraId")
+ manager.openCamera(
+ cameraId,
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraDevice.StateCallback() {
+ override fun onOpened(device: CameraDevice) = cont.resume(device)
+
+ override fun onDisconnected(device: CameraDevice) {
+ Log.w(TAG, "Camera $cameraId has been disconnected")
+ finish()
+ }
+
+ override fun onClosed(camera: CameraDevice) {
+ Log.d(TAG, "Camera - onClosed: $cameraId")
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartCamera) {
+ restartCamera = false
+ updateExtensionInfo()
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+ }
+
+ override fun onError(device: CameraDevice, error: Int) {
+ Log.d(TAG, "Camera - onError: $cameraId")
+ val msg = when (error) {
+ ERROR_CAMERA_DEVICE -> "Fatal (device)"
+ ERROR_CAMERA_DISABLED -> "Device policy"
+ ERROR_CAMERA_IN_USE -> "Camera in use"
+ ERROR_CAMERA_SERVICE -> "Fatal (service)"
+ ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+ else -> "Unknown"
+ }
+ val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
+ Log.e(TAG, exc.message, exc)
+ cont.resumeWithException(exc)
+ }
+ })
+ }
+
+ /**
+ * Opens and returns the extensions session (as the result of the suspend coroutine)
+ */
+ private suspend fun openCaptureSession(): CameraExtensionSession =
+ suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCaptureSession")
+ setupPreview()
+
+ if (stillImageReader != null) {
+ val imageReaderToClose = stillImageReader!!
+ imageSaveTerminationFuture.addListener(
+ { imageReaderToClose.close() },
+ mainExecutor
+ )
+ }
+
+ stillImageReader = setupImageReader()
+
+ val outputConfig = ArrayList<OutputConfiguration>()
+ outputConfig.add(OutputConfiguration(stillImageReader!!.surface))
+ outputConfig.add(OutputConfiguration(previewSurface))
+ val extensionConfiguration = ExtensionSessionConfiguration(
+ currentExtensionMode, outputConfig,
+ cameraTaskDispatcher.asExecutor(), object : CameraExtensionSession.StateCallback() {
+ override fun onClosed(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onClosed: $session")
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartPreview) {
+ restartPreview = false
+
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ cameraExtensionSession = openCaptureSession()
+ }
+ }
+ }
+ }
+
+ override fun onConfigured(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onConfigured: $session")
+ try {
+ val captureBuilder =
+ session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+ captureBuilder.addTarget(previewSurface)
+ session.setRepeatingRequest(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(), captureCallbacks
+ )
+ cont.resume(session)
+ runOnUiThread { enableUiControl(true) }
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(
+ RuntimeException("Failed to create capture session.")
+ )
+ }
+ }
+
+ override fun onConfigureFailed(session: CameraExtensionSession) {
+ Log.e(TAG, "CaptureSession - onConfigureFailed: $session")
+ cont.resumeWithException(
+ RuntimeException("Configure failed when creating capture session.")
+ )
+ }
+ }
+ )
+ try {
+ cameraDevice!!.createExtensionSession(extensionConfiguration)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(RuntimeException("Failed to create capture session."))
+ }
+ }
+
+ @Suppress("DEPRECATION") /* defaultDisplay */
+ private fun setupPreview() {
+ if (!textureView.isAvailable) {
+ Toast.makeText(
+ this, "TextureView is invalid!!",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ val previewResolution = pickPreviewResolution(
+ cameraManager,
+ currentCameraId,
+ resources.displayMetrics,
+ currentExtensionMode
+ )
+
+ if (previewResolution == null) {
+ Toast.makeText(
+ this,
+ "Invalid preview extension sizes!.",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ textureView.surfaceTexture?.setDefaultBufferSize(
+ previewResolution.width,
+ previewResolution.height
+ )
+ transformPreview(textureView, previewResolution, windowManager.defaultDisplay.rotation)
+ }
+
+ private fun setupImageReader(): ImageReader {
+ val (size, format) = pickStillImageResolution(
+ extensionCharacteristics,
+ currentExtensionMode
+ )
+
+ return ImageReader.newInstance(size.width, size.height, format, 1)
+ }
+
+ /**
+ * Takes a picture.
+ */
+ private fun takePicture() = lifecycleScope.launch(cameraTaskDispatcher) {
+ Preconditions.checkState(
+ cameraExtensionSession != null,
+ "take picture button is only enabled when session is configured successfully"
+ )
+ val session = cameraExtensionSession!!
+
+ var takePictureCompleter: Completer<Any?>? = null
+
+ imageSaveTerminationFuture = CallbackToFutureAdapter.getFuture<Any?> {
+ takePictureCompleter = it
+ "imageSaveTerminationFuture"
+ }
+
+ stillImageReader!!.setOnImageAvailableListener(
+ { reader: ImageReader ->
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ acquireImageAndSave(reader)
+ stillImageReader!!.setOnImageAvailableListener(null, null)
+ takePictureCompleter?.set(null)
+ lifecycleScope.launch(Dispatchers.Main) {
+ enableUiControl(true)
+ }
+ }
+ }, Handler(Looper.getMainLooper())
+ )
+
+ val captureBuilder = session.device.createCaptureRequest(
+ CameraDevice.TEMPLATE_STILL_CAPTURE
+ )
+ captureBuilder.addTarget(stillImageReader!!.surface)
+
+ session.capture(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraExtensionSession.ExtensionCaptureCallback() {
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ takePictureCompleter?.set(null)
+ Log.e(TAG, "Failed to take picture.")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+ }
+ )
+ }
+
+ /**
+ * Acquires the latest image from the image reader and save it to the Pictures folder
+ */
+ private fun acquireImageAndSave(imageReader: ImageReader) {
+ try {
+ val formatter: Format =
+ SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ val fileName =
+ "[${formatter.format(Calendar.getInstance().time)}][Camera2]${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+
+ val rotationDegrees = calculateRelativeImageRotationDegrees(
+ (getDisplayRotationDegrees(display!!.rotation)),
+ cameraSensorRotationDegrees,
+ currentCameraId == backCameraId
+ )
+
+ imageReader.acquireLatestImage().let { image ->
+ val uri = FileUtil.saveImage(
+ image,
+ fileName,
+ ".jpg",
+ "Pictures/ExtensionsPictures",
+ contentResolver,
+ rotationDegrees
+ )
+
+ image.close()
+
+ val msg = if (uri != null) {
+ "Saved image to $fileName.jpg"
+ } else {
+ "Failed to save image."
+ }
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ Toast.makeText(this@Camera2ExtensionsActivity, msg, Toast.LENGTH_SHORT).show()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val inflater = menuInflater
+ inflater.inflate(R.menu.main_menu_camera2_extensions_activity, menu)
+
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_camerax_extensions -> {
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return true
+ }
+ R.id.menu_validation_tool -> {
+ closeCameraAndStartActivity(CameraValidationResultActivity::class.java.name)
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun closeCameraAndStartActivity(className: String) {
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+
+ val intent = Intent()
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+ intent.setClassName(this, className)
+ startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5a0c0ce..47d80f8 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -35,6 +35,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
+import android.view.ViewStub;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@@ -273,8 +274,8 @@
captureButton.setOnClickListener((view) -> {
resetTakePictureIdlingResource();
- String fileName = formatter.format(Calendar.getInstance().getTime())
- + extensionModeString + ".jpg";
+ String fileName = "[" + formatter.format(Calendar.getInstance().getTime())
+ + "][CameraX]" + extensionModeString + ".jpg";
File saveFile = new File(dir, fileName);
ImageCapture.OutputFileOptions outputFileOptions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -332,9 +333,9 @@
sendBroadcast(intent);
}
- Toast.makeText(getApplicationContext(),
- "Saved image to " + saveFile,
- Toast.LENGTH_SHORT).show();
+ Toast.makeText(CameraExtensionsActivity.this,
+ "Saved image to " + fileName,
+ Toast.LENGTH_LONG).show();
}
}
@@ -383,7 +384,9 @@
StrictMode.VmPolicy policy =
new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
StrictMode.setVmPolicy(policy);
- mPreviewView = findViewById(R.id.previewView);
+ ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
+ viewFinderStub.setLayoutResource(R.layout.full_previewview);
+ mPreviewView = (PreviewView) viewFinderStub.inflate();
mFrameInfo = findViewById(R.id.frameInfo);
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
setupPinchToZoomAndTapToFocus(mPreviewView);
@@ -427,19 +430,36 @@
@Override
public boolean onCreateOptionsMenu(@Nullable Menu menu) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main_menu, menu);
+ if (menu != null) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_menu, menu);
+
+ // Remove Camera2Extensions implementation entry if the device API level is less than 32
+ if (Build.VERSION.SDK_INT < 31) {
+ menu.removeItem(R.id.menu_camera2_extensions);
+ }
+ }
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.menu_validation_tool) {
- Intent intent = new Intent(this, CameraValidationResultActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
- return true;
+ Intent intent = new Intent();
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ switch (item.getItemId()) {
+ case R.id.menu_camera2_extensions:
+ if (Build.VERSION.SDK_INT >= 31) {
+ mCameraProvider.unbindAll();
+ intent.setClassName(this, Camera2ExtensionsActivity.class.getName());
+ startActivity(intent);
+ finish();
+ }
+ return true;
+ case R.id.menu_validation_tool:
+ intent.setClassName(this, CameraValidationResultActivity.class.getName());
+ startActivity(intent);
+ finish();
+ return true;
}
return super.onOptionsItemSelected(item);
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
new file mode 100644
index 0000000..8bbd835
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
@@ -0,0 +1,347 @@
+/*
+ * 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.camera.integration.extensions.utils
+
+import android.annotation.SuppressLint
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.Size
+import android.view.Surface
+import android.view.TextureView
+import androidx.annotation.RequiresApi
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.stream.Collectors
+
+private const val TAG = "Camera2ExtensionsUtil"
+
+/**
+ * Util functions for Camera2 Extensions implementation
+ */
+object Camera2ExtensionsUtil {
+
+ /**
+ * Converts extension mode from integer to string.
+ */
+ @Suppress("DEPRECATION") // EXTENSION_BEAUTY
+ @JvmStatic
+ fun getExtensionModeStringFromId(extension: Int): String {
+ return when (extension) {
+ CameraExtensionCharacteristics.EXTENSION_HDR -> "HDR"
+ CameraExtensionCharacteristics.EXTENSION_NIGHT -> "NIGHT"
+ CameraExtensionCharacteristics.EXTENSION_BOKEH -> "BOKEH"
+ CameraExtensionCharacteristics.EXTENSION_BEAUTY -> "FACE RETOUCH"
+ else -> "AUTO"
+ }
+ }
+
+ /**
+ * Gets the first camera id of the specified lens facing.
+ */
+ @JvmStatic
+ fun getLensFacingCameraId(cameraManager: CameraManager, lensFacing: Int): String {
+ cameraManager.cameraIdList.forEach { cameraId ->
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ if (characteristics[CameraCharacteristics.LENS_FACING] == lensFacing) {
+ characteristics[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]?.let {
+ if (it.contains(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
+ )
+ ) {
+ return cameraId
+ }
+ }
+ }
+ }
+
+ throw IllegalArgumentException("Can't find camera of lens facing $lensFacing")
+ }
+
+ /**
+ * Creates a default extension capture callback implementation.
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun createExtensionCaptureCallback(): ExtensionCaptureCallback {
+ return object : ExtensionCaptureCallback() {
+ override fun onCaptureStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ timestamp: Long
+ ) {
+ }
+
+ override fun onCaptureProcessStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ }
+
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ Log.v(TAG, "onCaptureProcessFailed")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+
+ override fun onCaptureSequenceAborted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId")
+ }
+ }
+ }
+
+ /**
+ * Picks a preview resolution that is both close/same as the display size and supported by camera
+ * and extensions.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickPreviewResolution(
+ cameraManager: CameraManager,
+ cameraId: String,
+ displayMetrics: DisplayMetrics,
+ extensionMode: Int
+ ): Size? {
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ val map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )
+ val textureSizes = map!!.getOutputSizes(
+ SurfaceTexture::class.java
+ )
+ val displaySize = Point()
+ displaySize.x = displayMetrics.widthPixels
+ displaySize.y = displayMetrics.heightPixels
+ if (displaySize.x < displaySize.y) {
+ displaySize.x = displayMetrics.heightPixels
+ displaySize.y = displayMetrics.widthPixels
+ }
+ val displayArRatio = displaySize.x.toFloat() / displaySize.y
+ val previewSizes = ArrayList<Size>()
+ for (sz in textureSizes) {
+ val arRatio = sz.width.toFloat() / sz.height
+ if (Math.abs(arRatio - displayArRatio) <= .2f) {
+ previewSizes.add(sz)
+ }
+ }
+ val extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ val extensionSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, SurfaceTexture::class.java
+ )
+ if (extensionSizes.isEmpty()) {
+ return null
+ }
+
+ var previewSize = extensionSizes[0]
+ val supportedPreviewSizes =
+ previewSizes.stream().distinct().filter { o: Size -> extensionSizes.contains(o) }
+ .collect(Collectors.toList())
+ if (supportedPreviewSizes.isNotEmpty()) {
+ var currentDistance = Int.MAX_VALUE
+ for (sz in supportedPreviewSizes) {
+ val distance = Math.abs(sz.width * sz.height - displaySize.x * displaySize.y)
+ if (currentDistance > distance) {
+ currentDistance = distance
+ previewSize = sz
+ }
+ }
+ } else {
+ Log.w(
+ TAG, "No overlap between supported camera and extensions preview sizes using" +
+ " first available!"
+ )
+ }
+
+ return previewSize
+ }
+
+ /**
+ * Picks a resolution for still image capture.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickStillImageResolution(
+ extensionCharacteristics: CameraExtensionCharacteristics,
+ extensionMode: Int
+ ): Pair<Size, Int> {
+ val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.YUV_420_888
+ )
+ val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.JPEG
+ )
+ val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG
+ val stillCaptureSize =
+ if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0]
+
+ return Pair(stillCaptureSize, stillFormat)
+ }
+
+ /**
+ * Transforms the texture view to display the content of resolution in correct direction and
+ * aspect ratio.
+ */
+ @JvmStatic
+ fun transformPreview(textureView: TextureView, resolution: Size, displayRotation: Int) {
+ if (resolution.width == 0 || resolution.height == 0) {
+ return
+ }
+ if (textureView.width == 0 || textureView.height == 0) {
+ return
+ }
+ val matrix = Matrix()
+ val left: Int = textureView.left
+ val right: Int = textureView.right
+ val top: Int = textureView.top
+ val bottom: Int = textureView.bottom
+
+ // Compute the preview ui size based on the available width, height, and ui orientation.
+ val viewWidth = right - left
+ val viewHeight = bottom - top
+ val displayRotationDegrees: Int = getDisplayRotationDegrees(displayRotation)
+ val scaled: Size = calculatePreviewViewDimens(
+ resolution, viewWidth, viewHeight, displayRotation
+ )
+
+ // Compute the center of the view.
+ val centerX = (viewWidth / 2).toFloat()
+ val centerY = (viewHeight / 2).toFloat()
+
+ // Do corresponding rotation to correct the preview direction
+ matrix.postRotate((-displayRotationDegrees).toFloat(), centerX, centerY)
+
+ // Compute the scale value for center crop mode
+ var xScale = scaled.width / viewWidth.toFloat()
+ var yScale = scaled.height / viewHeight.toFloat()
+ if (displayRotationDegrees % 180 == 90) {
+ xScale = scaled.width / viewHeight.toFloat()
+ yScale = scaled.height / viewWidth.toFloat()
+ }
+
+ // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+ // two digits floating value to do the scale operation. Otherwise, the result may be scaled
+ // not large enough and will have some blank lines on the screen.
+ xScale = BigDecimal(xScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+ yScale = BigDecimal(yScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+
+ // Do corresponding scale to resolve the deformation problem
+ matrix.postScale(xScale, yScale, centerX, centerY)
+ textureView.setTransform(matrix)
+ }
+
+ /**
+ * Converts the display rotation to degrees value.
+ *
+ * @return One of 0, 90, 180, 270.
+ */
+ @JvmStatic
+ fun getDisplayRotationDegrees(displayRotation: Int): Int = when (displayRotation) {
+ Surface.ROTATION_0 -> 0
+ Surface.ROTATION_90 -> 90
+ Surface.ROTATION_180 -> 180
+ Surface.ROTATION_270 -> 270
+ else -> throw UnsupportedOperationException(
+ "Unsupported display rotation: $displayRotation"
+ )
+ }
+
+ /**
+ * Calculates the delta between a source rotation and destination rotation.
+ *
+ * <p>A typical use of this method would be calculating the angular difference between the
+ * display orientation (destRotationDegrees) and camera sensor orientation
+ * (sourceRotationDegrees).
+ *
+ * @param destRotationDegrees The destination rotation relative to the device's natural
+ * rotation.
+ * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+ * @param isOppositeFacing Whether the source and destination planes are facing opposite
+ * directions.
+ */
+ @JvmStatic
+ fun calculateRelativeImageRotationDegrees(
+ destRotationDegrees: Int,
+ sourceRotationDegrees: Int,
+ isOppositeFacing: Boolean
+ ): Int {
+ val result: Int = if (isOppositeFacing) {
+ (sourceRotationDegrees - destRotationDegrees + 360) % 360
+ } else {
+ (sourceRotationDegrees + destRotationDegrees) % 360
+ }
+
+ return result
+ }
+
+ /**
+ * Calculates the preview size which can display the source image in correct aspect ratio.
+ */
+ @JvmStatic
+ private fun calculatePreviewViewDimens(
+ srcSize: Size,
+ parentWidth: Int,
+ parentHeight: Int,
+ displayRotation: Int
+ ): Size {
+ var inWidth = srcSize.width
+ var inHeight = srcSize.height
+ if (displayRotation == 0 || displayRotation == 180) {
+ // Need to reverse the width and height since we're in landscape orientation.
+ inWidth = srcSize.height
+ inHeight = srcSize.width
+ }
+ var outWidth = parentWidth
+ var outHeight = parentHeight
+ if (inWidth != 0 && inHeight != 0) {
+ val vfRatio = inWidth / inHeight.toFloat()
+ val parentRatio = parentWidth / parentHeight.toFloat()
+
+ // Match shortest sides together.
+ if (vfRatio < parentRatio) {
+ outWidth = parentWidth
+ outHeight = Math.round(parentWidth / vfRatio)
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio)
+ outHeight = parentHeight
+ }
+ }
+ return Size(outWidth, outHeight)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
new file mode 100644
index 0000000..dbc661d
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.camera.integration.extensions.utils
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.ImageFormat
+import android.media.Image
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import androidx.camera.core.impl.utils.Exif
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+private const val TAG = "FileUtil"
+
+/**
+ * File util functions
+ */
+object FileUtil {
+
+ /**
+ * Saves an [Image] to the specified file path. The format of the input [Image] must be JPEG or
+ * YUV_420_888 format.
+ */
+ @JvmStatic
+ fun saveImage(
+ image: Image,
+ fileNamePrefix: String,
+ fileNameSuffix: String,
+ relativePath: String,
+ contentResolver: ContentResolver,
+ rotationDegrees: Int
+ ): Uri? {
+ require((image.format == ImageFormat.JPEG) or (image.format == ImageFormat.YUV_420_888)) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val fileName = if (fileNameSuffix.isNotEmpty() && fileNameSuffix[0] == '.') {
+ fileNamePrefix + fileNameSuffix
+ } else {
+ "$fileNamePrefix.$fileNameSuffix"
+ }
+
+ // Saves the image to the temp file
+ val tempFileUri =
+ saveImageToTempFile(image, fileNamePrefix, fileNameSuffix) ?: return null
+
+ // Updates Exif rotation tag info
+ val exif = Exif.createFromFile(tempFileUri.toFile())
+ exif.rotate(rotationDegrees)
+ exif.save()
+
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+ }
+
+ // Copies the temp file to the final output path
+ return copyTempFileToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ }
+
+ /**
+ * Saves an [Image] to a temp file.
+ */
+ @JvmStatic
+ fun saveImageToTempFile(
+ image: Image,
+ prefix: String,
+ suffix: String,
+ cacheDir: File? = null
+ ): Uri? {
+ val tempFile = File.createTempFile(
+ prefix,
+ suffix,
+ cacheDir
+ )
+
+ val byteArray = when (image.format) {
+ ImageFormat.JPEG -> {
+ ImageUtil.jpegImageToJpegByteArray(image)
+ }
+ ImageFormat.YUV_420_888 -> {
+ ImageUtil.yuvImageToJpegByteArray(image, 100)
+ }
+ else -> {
+ Log.e(TAG, "Incorrect image format of the input image proxy: ${image.format}")
+ return null
+ }
+ }
+
+ val outputStream = FileOutputStream(tempFile)
+ outputStream.write(byteArray)
+ outputStream.close()
+
+ return tempFile.toUri()
+ }
+
+ /**
+ * Copies temp file to the destination location.
+ *
+ * @return null if the copy process is failed.
+ */
+ @JvmStatic
+ fun copyTempFileToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ targetUrl: Uri,
+ contentValues: ContentValues,
+ ): Uri? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ Log.e(TAG, "The known devices which support Extensions should be at least" +
+ " Android Q!")
+ return null
+ }
+
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+ val outputUri = contentResolver.insert(targetUrl, contentValues) ?: return null
+
+ if (copyTempFileByteArrayToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ outputUri
+ )
+ ) {
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+ contentResolver.update(outputUri, contentValues, null, null)
+ return outputUri
+ } else {
+ Log.e(TAG, "Failed to copy the temp file to the output path!")
+ }
+
+ return null
+ }
+
+ /**
+ * Copies temp file byte array to output [Uri].
+ *
+ * @return false if the [Uri] is not writable.
+ */
+ @JvmStatic
+ private fun copyTempFileByteArrayToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ uri: Uri
+ ): Boolean {
+ contentResolver.openOutputStream(uri).use { outputStream ->
+ if (tempFileUri.path == null || outputStream == null) {
+ return false
+ }
+
+ val tempFile = File(tempFileUri.path!!)
+
+ FileInputStream(tempFile).use { inputStream ->
+ val buf = ByteArray(1024)
+ var len: Int
+ while (inputStream.read(buf).also { len = it } > 0) {
+ outputStream.write(buf, 0, len)
+ }
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
new file mode 100644
index 0000000..70c1cb5
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.camera.integration.extensions.utils
+
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.Image
+import androidx.annotation.IntRange
+import androidx.camera.core.ImageProxy
+import java.io.ByteArrayOutputStream
+
+/**
+ * Image util functions
+ */
+object ImageUtil {
+
+ /**
+ * Converts JPEG [Image] to [ByteArray]
+ */
+ @JvmStatic
+ fun jpegImageToJpegByteArray(image: Image): ByteArray {
+ require(image.format == ImageFormat.JPEG) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ val planes = image.planes
+ val buffer = planes[0].buffer
+ val data = ByteArray(buffer.capacity())
+ buffer.rewind()
+ buffer[data]
+ return data
+ }
+
+ /**
+ * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image
+ * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
+ * be compressed by the specified quality value.
+ */
+ @JvmStatic
+ fun yuvImageToJpegByteArray(
+ image: Image,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ return nv21ToJpeg(
+ yuv_420_888toNv21(image),
+ image.width,
+ image.height,
+ jpegQuality
+ )
+ }
+
+ /**
+ * Converts nv21 byte array to JPEG format.
+ */
+ @JvmStatic
+ private fun nv21ToJpeg(
+ nv21: ByteArray,
+ width: Int,
+ height: Int,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ val out = ByteArrayOutputStream()
+ val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+ val success = yuv.compressToJpeg(Rect(0, 0, width, height), jpegQuality, out)
+
+ if (!success) {
+ throw RuntimeException("YuvImage failed to encode jpeg.")
+ }
+ return out.toByteArray()
+ }
+
+ /**
+ * Converts a YUV [Image] to NV21 byte array.
+ */
+ @JvmStatic
+ private fun yuv_420_888toNv21(image: Image): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val yPlane = image.planes[0]
+ val uPlane = image.planes[1]
+ val vPlane = image.planes[2]
+ val yBuffer = yPlane.buffer
+ val uBuffer = uPlane.buffer
+ val vBuffer = vPlane.buffer
+ yBuffer.rewind()
+ uBuffer.rewind()
+ vBuffer.rewind()
+ val ySize = yBuffer.remaining()
+ var position = 0
+ // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+ val nv21 = ByteArray(ySize + image.width * image.height / 2)
+
+ // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+ for (row in 0 until image.height) {
+ yBuffer[nv21, position, image.width]
+ position += image.width
+ yBuffer.position(
+ Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride)
+ )
+ }
+ val chromaHeight = image.height / 2
+ val chromaWidth = image.width / 2
+ val vRowStride = vPlane.rowStride
+ val uRowStride = uPlane.rowStride
+ val vPixelStride = vPlane.pixelStride
+ val uPixelStride = uPlane.pixelStride
+
+ // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+ // perform faster bulk gets from the byte buffers.
+ val vLineBuffer = ByteArray(vRowStride)
+ val uLineBuffer = ByteArray(uRowStride)
+ for (row in 0 until chromaHeight) {
+ vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
+ uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
+ var vLineBufferPosition = 0
+ var uLineBufferPosition = 0
+ for (col in 0 until chromaWidth) {
+ nv21[position++] = vLineBuffer[vLineBufferPosition]
+ nv21[position++] = uLineBuffer[uLineBufferPosition]
+ vLineBufferPosition += vPixelStride
+ uLineBufferPosition += uPixelStride
+ }
+ }
+ return nv21
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 33c44a1..013ef13 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -19,7 +19,6 @@
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
-import android.graphics.ImageFormat
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
@@ -30,11 +29,13 @@
import android.widget.Button
import android.widget.ImageButton
import android.widget.Toast
+import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraInfo
import androidx.camera.core.DisplayOrientedMeteringPointFactory
+import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
@@ -51,6 +52,7 @@
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -66,14 +68,11 @@
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
-import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch
-import java.io.File
-import java.io.FileOutputStream
private const val TAG = "ImageCaptureActivity"
@@ -196,6 +195,7 @@
}
}
+ @OptIn(markerClass = [ExperimentalGetImage::class])
private fun setupUiControls() {
// Sets up the flash toggle button
setUpFlashButton()
@@ -227,21 +227,22 @@
} else {
"$filenamePrefix[Disabled]"
}
- val tempFile = File.createTempFile(
- filename,
- "",
- codeCacheDir
- )
- val outputStream = FileOutputStream(tempFile)
- val byteArray = jpegImageToJpegByteArray(image)
- outputStream.write(byteArray)
- outputStream.close()
- result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, tempFile.toUri())
- result.putExtra(
- INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
- image.imageInfo.rotationDegrees
- )
+ val uri =
+ FileUtil.saveImageToTempFile(image.image!!, filename, "", cacheDir)
+
+ if (uri == null) {
+ result.putExtra(
+ INTENT_EXTRA_KEY_ERROR_CODE,
+ ERROR_CODE_SAVE_IMAGE_FAILED
+ )
+ } else {
+ result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, uri)
+ result.putExtra(
+ INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
+ image.imageInfo.rotationDegrees
+ )
+ }
finish()
}
@@ -456,25 +457,11 @@
extensionToggleButton.setImageResource(resourceId)
}
- /**
- * Converts JPEG [ImageProxy] to JPEG byte array.
- */
- internal fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray {
- require(image.format == ImageFormat.JPEG) {
- "Incorrect image format of the input image proxy: ${image.format}"
- }
- val planes = image.planes
- val buffer = planes[0].buffer
- val data = ByteArray(buffer.capacity())
- buffer.rewind()
- buffer[data]
- return data
- }
-
companion object {
const val ERROR_CODE_NONE = 0
const val ERROR_CODE_BIND_FAIL = 1
const val ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT = 2
const val ERROR_CODE_TAKE_PICTURE_FAILED = 3
+ const val ERROR_CODE_SAVE_IMAGE_FAILED = 4
}
}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 5e01f7a..2782d05 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -35,6 +35,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -48,13 +49,13 @@
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
+import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_SAVE_IMAGE_FAILED
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
-import androidx.camera.integration.extensions.validation.TestResults.Companion.copyTempFileToOutputLocation
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
@@ -127,7 +128,8 @@
// Returns with error
if (errorCode == ERROR_CODE_BIND_FAIL ||
errorCode == ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT ||
- errorCode == ERROR_CODE_TAKE_PICTURE_FAILED
+ errorCode == ERROR_CODE_TAKE_PICTURE_FAILED ||
+ errorCode == ERROR_CODE_SAVE_IMAGE_FAILED
) {
result.putExtra(INTENT_EXTRA_KEY_TEST_RESULT, TEST_RESULT_FAILED)
Log.e(TAG, "Failed to take a picture with error code: $errorCode")
@@ -196,13 +198,14 @@
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/ExtensionsValidation")
}
- if (copyTempFileToOutputLocation(
- contentResolver,
- imageUris[viewPager.currentItem].first,
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues
- )
- ) {
+ val outputUri = copyTempFileToOutputLocation(
+ contentResolver,
+ imageUris[viewPager.currentItem].first,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+
+ if (outputUri != null) {
Toast.makeText(
this,
"Image is saved as Pictures/ExtensionsValidation/$savedFileName.",
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
index c4becdf4..1a6fa1b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
@@ -20,8 +20,6 @@
import android.content.ContentValues
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
-import android.net.Uri
-import android.os.Build
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.provider.MediaStore
import android.util.Log
@@ -33,6 +31,7 @@
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeIdFromString
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.net.toUri
import java.io.BufferedReader
@@ -127,7 +126,7 @@
testResultsFile.toUri(),
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
contentValues
- )
+ ) != null
) {
return "$DIRECTORY_DOCUMENTS/ExtensionsValidation/$savedFileName"
}
@@ -240,72 +239,6 @@
}
companion object {
-
- /**
- * Copies temp file to the destination location.
- *
- * @return false if the copy process is failed.
- */
- fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- targetUrl: Uri,
- contentValues: ContentValues,
- ): Boolean {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- Log.e(TAG, "The known devices which support Extensions should be at least" +
- " Android Q!")
- return false
- }
-
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
-
- val outputUri = contentResolver.insert(targetUrl, contentValues)
-
- if (outputUri != null && copyTempFileToOutputLocation(
- contentResolver,
- tempFileUri,
- outputUri
- )
- ) {
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
- contentResolver.update(outputUri, contentValues, null, null)
- return true
- } else {
- Log.e(TAG, "Failed to copy the temp file to the output path!")
- }
-
- return false
- }
-
- /**
- * Copies temp file to output [Uri].
- *
- * @return false if the [Uri] is not writable.
- */
- private fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- uri: Uri
- ): Boolean {
- contentResolver.openOutputStream(uri).use { outputStream ->
- if (tempFileUri.path == null || outputStream == null) {
- return false
- }
-
- val tempFile = File(tempFileUri.path!!)
-
- FileInputStream(tempFile).use { `in` ->
- val buf = ByteArray(1024)
- var len: Int
- while (`in`.read(buf).also { len = it } > 0) {
- outputStream.write(buf, 0, len)
- }
- }
- }
- return true
- }
-
const val INVALID_EXTENSION_MODE = -1
const val TEST_RESULT_NOT_SUPPORTED = -1
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
index 9ed60c5..1a8adfa 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
@@ -24,14 +24,14 @@
android:layout_height="match_parent"
tools:context="androidx.camera.integration.extensions.CameraExtensionsActivity">
- <androidx.camera.view.PreviewView
- android:id="@+id/previewView"
+ <ViewStub
+ android:id="@+id/viewFinderStub"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
new file mode 100644
index 0000000..4c0f530
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
new file mode 100644
index 0000000..18ebf88
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<TextureView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
index dc9d084..7fe8504 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
@@ -16,6 +16,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
+ android:id="@+id/menu_camera2_extensions"
+ android:title="Camera2 Extensions" />
+ <item
android:id="@+id/menu_validation_tool"
android:title="Validation Tool" />
</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
new file mode 100644
index 0000000..bc8bdba
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 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.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_camerax_extensions"
+ android:title="CameraX Extensions" />
+ <item
+ android:id="@+id/menu_validation_tool"
+ android:title="Validation Tool" />
+</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 072b0f5..9c8eef1 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -67,6 +67,7 @@
implementation(project(":camera:camera-camera2"))
implementation(project(":camera:camera-lifecycle"))
implementation(project(":camera:camera-view"))
+ implementation(project(":camera:camera-video"))
// Android Support Library
implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
index cc6c150..0f0927f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
@@ -16,7 +16,6 @@
package androidx.camera.integration.uiwidgets.compose.ui.navigation
-import androidx.camera.integration.uiwidgets.compose.ui.screen.gallery.GalleryScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture.ImageCaptureScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture.VideoCaptureScreen
import androidx.compose.runtime.Composable
@@ -42,9 +41,5 @@
composable(ComposeCameraScreen.VideoCapture.name) {
VideoCaptureScreen()
}
-
- composable(ComposeCameraScreen.Gallery.name) {
- GalleryScreen()
- }
}
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
index 8923506..124fe4f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
@@ -18,7 +18,6 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
-import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.ui.graphics.vector.ImageVector
@@ -32,9 +31,6 @@
),
VideoCapture(
icon = Icons.Filled.Videocam
- ),
- Gallery(
- icon = Icons.Filled.PhotoLibrary
);
companion object {
@@ -42,7 +38,6 @@
return when (route?.substringBefore("/")) {
ImageCapture.name -> ImageCapture
VideoCapture.name -> VideoCapture
- Gallery.name -> Gallery
null -> defaultRoute
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
index 37c0d18..0f237e5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
@@ -20,8 +20,10 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@@ -32,6 +34,7 @@
imageVector: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
+ tint: Color = Color.Unspecified,
onClick: () -> Unit
) {
IconButton(
@@ -41,12 +44,24 @@
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
- modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE),
+ tint = tint
)
}
}
@Composable
+fun CameraControlText(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Text(
+ text = text,
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ )
+}
+
+@Composable
fun CameraControlButtonPlaceholder(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE))
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
index 866ca73..914a04e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
@@ -17,16 +17,24 @@
package androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture
import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButtonPlaceholder
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.FlipCameraAndroid
import androidx.compose.material.icons.sharp.Lens
@@ -35,7 +43,9 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -44,10 +54,54 @@
@Composable
fun ImageCaptureScreen(
modifier: Modifier = Modifier,
- stateHolder: ImageCaptureScreenStateHolder = rememberImageCaptureScreenStateHolder()
+ state: ImageCaptureScreenState = rememberImageCaptureScreenState()
) {
val lifecycleOwner = LocalLifecycleOwner.current
val localContext = LocalContext.current
+
+ LaunchedEffect(key1 = state.lensFacing) {
+ state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ }
+
+ ImageCaptureScreen(
+ modifier = modifier,
+ zoomRatio = state.zoomRatio,
+ linearZoom = state.linearZoom,
+ onLinearZoomChange = state::setLinearZoom,
+ isCameraReady = state.isCameraReady,
+ hasFlashUnit = state.hasFlashUnit,
+ flashModeIcon = state.flashModeIcon,
+ onFlashModeIconClicked = state::toggleFlashMode,
+ onFlipCameraIconClicked = state::toggleLensFacing,
+ onImageCaptureIconClicked = {
+ state.takePhoto(localContext)
+ },
+ onSurfaceProviderReady = state::setSurfaceProvider,
+ onTouch = state::startTapToFocus
+ )
+}
+
+@Composable
+fun ImageCaptureScreen(
+ modifier: Modifier,
+ zoomRatio: Float,
+ linearZoom: Float,
+ onLinearZoomChange: (Float) -> Unit,
+ isCameraReady: Boolean,
+ hasFlashUnit: Boolean,
+ flashModeIcon: ImageVector,
+ onFlashModeIconClicked: () -> Unit,
+ onFlipCameraIconClicked: () -> Unit,
+ onImageCaptureIconClicked: () -> Unit,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+ onTouch: (MeteringPoint) -> Unit
+) {
+ val localContext = LocalContext.current
+
+ // Saving an instance of PreviewView outside of AndroidView
+ // This allows us to access properties of PreviewView (e.g. ViewPort and OutputTransform)
+ // Allows us to support functionalities such as UseCaseGroup in bindToLifecycle()
+ // This instance needs to be carefully used in controlled environments (e.g. LaunchedEffect)
val previewView = remember {
PreviewView(localContext).apply {
layoutParams = ViewGroup.LayoutParams(
@@ -55,51 +109,78 @@
ViewGroup.LayoutParams.MATCH_PARENT
)
- stateHolder.setSurfaceProvider(this.surfaceProvider)
- }
- }
+ onSurfaceProviderReady(this.surfaceProvider)
- LaunchedEffect(key1 = stateHolder.lensFacing) {
- stateHolder.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ setOnTouchListener { view, motionEvent ->
+ val meteringPointFactory = (view as PreviewView).meteringPointFactory
+ val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+ onTouch(meteringPoint)
+
+ return@setOnTouchListener true
+ }
+ }
}
Box(modifier = modifier.fillMaxSize()) {
AndroidView(
- factory = {
- previewView
- }
+ factory = { previewView }
)
- CameraControlRow(modifier = Modifier.align(Alignment.BottomCenter)) {
- CameraControlButton(
- imageVector = Icons.Sharp.FlipCameraAndroid,
- contentDescription = "Toggle Camera Lens",
- ) {
- stateHolder.toggleLensFacing()
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+
+ // Display Zoom Slider only when Camera is ready
+ if (isCameraReady) {
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(modifier = Modifier.weight(1f)) {
+ Slider(
+ value = linearZoom,
+ onValueChange = onLinearZoomChange
+ )
+ }
+
+ Text(
+ text = "%.2f x".format(zoomRatio),
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .background(Color.White)
+ )
+ }
}
- CameraControlButton(
- imageVector = Icons.Sharp.Lens,
- contentDescription = "Image Capture",
- modifier = Modifier
- .padding(1.dp)
- .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape)
- ) {
- stateHolder.takePhoto(localContext)
- }
-
- if (stateHolder.hasFlashMode) {
+ CameraControlRow {
CameraControlButton(
- imageVector = stateHolder.flashModeIcon,
- contentDescription = "Toggle Flash Mode",
+ imageVector = Icons.Sharp.FlipCameraAndroid,
+ contentDescription = "Toggle Camera Lens",
+ onClick = onFlipCameraIconClicked
+ )
+
+ CameraControlButton(
+ imageVector = Icons.Sharp.Lens,
+ contentDescription = "Image Capture",
modifier = Modifier
.padding(1.dp)
- .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape)
- ) {
- stateHolder.toggleFlashMode()
+ .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+ onClick = onImageCaptureIconClicked
+ )
+
+ if (hasFlashUnit) {
+ CameraControlButton(
+ imageVector = flashModeIcon,
+ contentDescription = "Toggle Flash Mode",
+ modifier = Modifier
+ .padding(1.dp)
+ .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape),
+ onClick = onFlashModeIconClicked
+ )
+ } else {
+ CameraControlButtonPlaceholder()
}
- } else {
- CameraControlButtonPlaceholder()
}
}
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
similarity index 71%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
rename to camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
index a3d1349..ee8bd3c 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
@@ -22,9 +22,13 @@
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl.OperationCanceledException
import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.MeteringPoint
import androidx.camera.core.Preview
import androidx.camera.core.Preview.SurfaceProvider
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -40,22 +44,28 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.text.SimpleDateFormat
import java.util.Locale
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
private const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
-class ImageCaptureScreenStateHolder(
+class ImageCaptureScreenState(
initialLensFacing: Int = DEFAULT_LENS_FACING,
initialFlashMode: Int = DEFAULT_FLASH_MODE
) {
var lensFacing by mutableStateOf(initialLensFacing)
private set
- var hasFlashMode by mutableStateOf(false)
+ var hasFlashUnit by mutableStateOf(false)
+ private set
+
+ var isCameraReady by mutableStateOf(false)
private set
var flashMode: Int by mutableStateOf(getValidInitialFlashMode(initialFlashMode))
@@ -65,17 +75,49 @@
private set
get() = getFlashModeImageVector()
+ var linearZoom by mutableStateOf(0f)
+ private set
+
+ var zoomRatio by mutableStateOf(1f)
+ private set
+
private val preview = Preview.Builder().build()
private val imageCapture = ImageCapture
.Builder()
.setFlashMode(flashMode)
.build()
+ private var camera: Camera? = null
+
+ private val mainScope = MainScope()
+
fun setSurfaceProvider(surfaceProvider: SurfaceProvider) {
Log.d(TAG, "Setting Surface Provider")
preview.setSurfaceProvider(surfaceProvider)
}
+ @JvmName("setLinearZoomFunction")
+ fun setLinearZoom(linearZoom: Float) {
+ Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set Linear Zoom")
+ return
+ }
+
+ val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+ mainScope.launch {
+ try {
+ future.await()
+ } catch (exc: Exception) {
+ // Log errors not related to CameraControl.OperationCanceledException
+ if (exc !is OperationCanceledException) {
+ Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+ }
+ }
+ }
+ }
+
fun toggleLensFacing() {
Log.d(TAG, "Toggling Lens")
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
@@ -104,8 +146,14 @@
imageCapture.flashMode = flashMode
}
+ fun startTapToFocus(meteringPoint: MeteringPoint) {
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+
fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
Log.d(TAG, "Starting Camera")
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
@@ -116,6 +164,14 @@
.requireLensFacing(lensFacing)
.build()
+ // Remove observers from the old camera instance
+ removeZoomStateObservers(lifecycleOwner)
+
+ // Reset internal State of Camera
+ camera = null
+ hasFlashUnit = false
+ isCameraReady = false
+
try {
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(
@@ -126,7 +182,10 @@
)
// Setup components that require Camera
- this.hasFlashMode = camera.cameraInfo.hasFlashUnit()
+ this.camera = camera
+ setupZoomStateObserver(lifecycleOwner)
+ hasFlashUnit = camera.cameraInfo.hasFlashUnit()
+ isCameraReady = true
} catch (exc: Exception) {
Log.e(TAG, "Use Cases binding failed", exc)
}
@@ -201,6 +260,32 @@
}
}
+ private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Setting up Zoom State Observer")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set up observer")
+ return
+ }
+
+ removeZoomStateObservers(lifecycleOwner)
+ camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+ linearZoom = state.linearZoom
+ zoomRatio = state.zoomRatio
+ }
+ }
+
+ private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Removing Observers")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not present to remove observers")
+ return
+ }
+
+ camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+ }
+
companion object {
private const val TAG = "ImageCaptureScreenState"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
@@ -209,12 +294,12 @@
ImageCapture.FLASH_MODE_OFF,
ImageCapture.FLASH_MODE_AUTO
)
- val saver: Saver<ImageCaptureScreenStateHolder, *> = listSaver(
+ val saver: Saver<ImageCaptureScreenState, *> = listSaver(
save = {
listOf(it.lensFacing, it.flashMode)
},
restore = {
- ImageCaptureScreenStateHolder(
+ ImageCaptureScreenState(
initialLensFacing = it[0],
initialFlashMode = it[1]
)
@@ -224,16 +309,16 @@
}
@Composable
-fun rememberImageCaptureScreenStateHolder(
+fun rememberImageCaptureScreenState(
initialLensFacing: Int = DEFAULT_LENS_FACING,
initialFlashMode: Int = DEFAULT_FLASH_MODE
-): ImageCaptureScreenStateHolder {
+): ImageCaptureScreenState {
return rememberSaveable(
initialLensFacing,
initialFlashMode,
- saver = ImageCaptureScreenStateHolder.saver
+ saver = ImageCaptureScreenState.saver
) {
- ImageCaptureScreenStateHolder(
+ ImageCaptureScreenState(
initialLensFacing = initialLensFacing,
initialFlashMode = initialFlashMode
)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 726739f..acc60e6 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -16,10 +16,171 @@
package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
+import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlText
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.FlipCameraAndroid
+import androidx.compose.material.icons.sharp.Lens
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
@Composable
-fun VideoCaptureScreen() {
- Text("Video Capture Screen")
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ state: VideoCaptureScreenState = rememberVideoCaptureScreenState()
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val localContext = LocalContext.current
+
+ LaunchedEffect(key1 = state.lensFacing) {
+ state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ }
+
+ VideoCaptureScreen(
+ modifier = modifier,
+ zoomRatio = state.zoomRatio,
+ linearZoom = state.linearZoom,
+ onLinearZoomChange = state::setLinearZoom,
+ isCameraReady = state.isCameraReady,
+ recordState = state.recordState,
+ recordingStatsMsg = state.recordingStatsMsg,
+ onFlipCameraIconClicked = state::toggleLensFacing,
+ onVideoCaptureIconClicked = {
+ state.captureVideo(localContext)
+ },
+ onSurfaceProviderReady = state::setSurfaceProvider,
+ onTouch = state::startTapToFocus
+ )
+}
+
+@Composable
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ zoomRatio: Float,
+ linearZoom: Float,
+ onLinearZoomChange: (Float) -> Unit,
+ isCameraReady: Boolean,
+ recordState: VideoCaptureScreenState.RecordState,
+ recordingStatsMsg: String,
+ onFlipCameraIconClicked: () -> Unit,
+ onVideoCaptureIconClicked: () -> Unit,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+ onTouch: (MeteringPoint) -> Unit
+) {
+ val localContext = LocalContext.current
+
+ val previewView = remember {
+ PreviewView(localContext).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+
+ onSurfaceProviderReady(this.surfaceProvider)
+
+ setOnTouchListener { view, motionEvent ->
+ val meteringPointFactory = (view as PreviewView).meteringPointFactory
+ val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+ onTouch(meteringPoint)
+
+ return@setOnTouchListener true
+ }
+ }
+ }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ AndroidView(
+ factory = { previewView }
+ )
+
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+
+ // Display Zoom Slider only when Camera is ready
+ if (isCameraReady) {
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(modifier = Modifier.weight(1f)) {
+ Slider(
+ value = linearZoom,
+ onValueChange = onLinearZoomChange
+ )
+ }
+
+ Text(
+ text = "%.2f x".format(zoomRatio),
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .background(Color.White)
+ )
+ }
+ }
+
+ CameraControlRow {
+ CameraControlButton(
+ imageVector = Icons.Sharp.FlipCameraAndroid,
+ contentDescription = "Toggle Camera Lens",
+ onClick = onFlipCameraIconClicked
+ )
+
+ VideoRecordButton(
+ recordState = recordState,
+ onVideoCaptureIconClicked = onVideoCaptureIconClicked
+ )
+
+ CameraControlText(text = recordingStatsMsg)
+ }
+ }
+ }
+}
+
+@Composable
+private fun VideoRecordButton(
+ recordState: VideoCaptureScreenState.RecordState,
+ onVideoCaptureIconClicked: () -> Unit
+) {
+ val iconColor = when (recordState) {
+ VideoCaptureScreenState.RecordState.IDLE -> Color.Black
+ VideoCaptureScreenState.RecordState.RECORDING -> Color.Red
+ VideoCaptureScreenState.RecordState.STOPPING -> Color.Gray
+ }
+
+ CameraControlButton(
+ imageVector = Icons.Sharp.Lens,
+ contentDescription = "Video Capture",
+ modifier = Modifier
+ .padding(1.dp)
+ .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+ tint = iconColor,
+ onClick = onVideoCaptureIconClicked
+ )
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
new file mode 100644
index 0000000..bd6b3cc
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.camera.integration.uiwidgets.compose.ui.screen.videocapture
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
+import androidx.lifecycle.LifecycleOwner
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
+
+class VideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+) {
+ var lensFacing by mutableStateOf(initialLensFacing)
+ private set
+
+ var isCameraReady by mutableStateOf(false)
+ private set
+
+ var linearZoom by mutableStateOf(0f)
+ private set
+
+ var zoomRatio by mutableStateOf(1f)
+ private set
+
+ private var recording: Recording? = null
+
+ var recordState by mutableStateOf(RecordState.IDLE)
+ private set
+
+ var recordingStatsMsg by mutableStateOf("")
+ private set
+
+ private val preview = Preview.Builder().build()
+ private lateinit var recorder: Recorder
+ private lateinit var videoCapture: VideoCapture<Recorder>
+
+ private var camera: Camera? = null
+
+ private val mainScope = MainScope()
+
+ fun setSurfaceProvider(surfaceProvider: Preview.SurfaceProvider) {
+ Log.d(TAG, "Setting Surface Provider")
+ preview.setSurfaceProvider(surfaceProvider)
+ }
+
+ @JvmName("setLinearZoomFunction")
+ fun setLinearZoom(linearZoom: Float) {
+ Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set Linear Zoom")
+ return
+ }
+
+ val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+ mainScope.launch {
+ try {
+ future.await()
+ } catch (exc: Exception) {
+ // Log errors not related to CameraControl.OperationCanceledException
+ if (exc !is CameraControl.OperationCanceledException) {
+ Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+ }
+ }
+ }
+ }
+
+ fun toggleLensFacing() {
+ Log.d(TAG, "Toggling Lens")
+ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
+ CameraSelector.LENS_FACING_FRONT
+ } else {
+ CameraSelector.LENS_FACING_BACK
+ }
+ }
+
+ fun startTapToFocus(meteringPoint: MeteringPoint) {
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+
+ fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Starting Camera")
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider = cameraProviderFuture.get()
+
+ // Create a new recorder. CameraX currently does not support re-use of Recorder
+ recorder =
+ Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()
+ videoCapture = VideoCapture.withOutput(recorder)
+
+ val cameraSelector = CameraSelector
+ .Builder()
+ .requireLensFacing(lensFacing)
+ .build()
+
+ // Remove observers from the old camera instance
+ removeZoomStateObservers(lifecycleOwner)
+
+ // Reset internal State of Camera
+ camera = null
+ isCameraReady = false
+
+ try {
+ cameraProvider.unbindAll()
+ val camera = cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ videoCapture
+ )
+
+ this.camera = camera
+ setupZoomStateObserver(lifecycleOwner)
+ isCameraReady = true
+ } catch (exc: Exception) {
+ Log.e(TAG, "Use Cases binding failed", exc)
+ }
+ }, ContextCompat.getMainExecutor(context))
+ }
+
+ fun captureVideo(context: Context) {
+ Log.d(TAG, "Capture Video")
+
+ // Disable button if CameraX is already stopping the recording
+ if (recordState == RecordState.STOPPING) {
+ return
+ }
+
+ // Stop current recording session
+ val curRecording = recording
+ if (curRecording != null) {
+ Log.d(TAG, "Recording session exists. Stop recording")
+ recordState = RecordState.STOPPING
+ curRecording.stop()
+ return
+ }
+
+ Log.d(TAG, "Start recording video")
+ val mediaStoreOutputOptions = getMediaStoreOutputOptions(context)
+
+ recording = videoCapture.output
+ .prepareRecording(context, mediaStoreOutputOptions)
+ .apply {
+ val recordAudioPermission = PermissionChecker.checkSelfPermission(
+ context,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ if (recordAudioPermission == PermissionChecker.PERMISSION_GRANTED) {
+ withAudioEnabled()
+ }
+ }
+ .start(ContextCompat.getMainExecutor(context)) { recordEvent ->
+ // Update record stats
+ val recordingStats = recordEvent.recordingStats
+ val durationMs = TimeUnit.NANOSECONDS.toMillis(recordingStats.recordedDurationNanos)
+ val sizeMb = recordingStats.numBytesRecorded / (1000f * 1000f)
+ val msg = "%.2f s\n%.2f MB".format(durationMs / 1000f, sizeMb)
+ recordingStatsMsg = msg
+
+ when (recordEvent) {
+ is VideoRecordEvent.Start -> {
+ recordState = RecordState.RECORDING
+ }
+ is VideoRecordEvent.Finalize -> {
+ // Once finalized, save the file if it is created
+ val cause = recordEvent.cause
+ when (val errorCode = recordEvent.error) {
+ ERROR_NONE, ERROR_SOURCE_INACTIVE -> { // Save Output
+ val uri = recordEvent.outputResults.outputUri
+ val successMsg = "Video saved at $uri. Code: $errorCode"
+ Log.d(TAG, successMsg, cause)
+ Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show()
+ }
+ else -> { // Handle Error
+ val failureMsg = "VideoCapture Error($errorCode): $cause"
+ Log.e(TAG, failureMsg, cause)
+ }
+ }
+
+ // Tear down recording
+ recordState = RecordState.IDLE
+ recording = null
+ recordingStatsMsg = ""
+ }
+ }
+ }
+ }
+
+ private fun getMediaStoreOutputOptions(context: Context): MediaStoreOutputOptions {
+ val contentResolver = context.contentResolver
+ val displayName = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
+ .format(System.currentTimeMillis())
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
+ }
+ }
+
+ return MediaStoreOutputOptions
+ .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+ .setContentValues(contentValues)
+ .build()
+ }
+
+ private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Setting up Zoom State Observer")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set up observer")
+ return
+ }
+
+ removeZoomStateObservers(lifecycleOwner)
+ camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+ linearZoom = state.linearZoom
+ zoomRatio = state.zoomRatio
+ }
+ }
+
+ private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Removing Observers")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not present to remove observers")
+ return
+ }
+
+ camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+ }
+
+ enum class RecordState {
+ IDLE,
+ RECORDING,
+ STOPPING
+ }
+
+ companion object {
+ private const val TAG = "VideoCaptureScreenState"
+ private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
+ val saver: Saver<VideoCaptureScreenState, *> = listSaver(
+ save = {
+ listOf(it.lensFacing)
+ },
+ restore = {
+ VideoCaptureScreenState(
+ initialLensFacing = it[0]
+ )
+ }
+ )
+ }
+}
+
+@Composable
+fun rememberVideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+): VideoCaptureScreenState {
+ return rememberSaveable(
+ initialLensFacing,
+ saver = VideoCaptureScreenState.saver
+ ) {
+ VideoCaptureScreenState(
+ initialLensFacing = initialLensFacing
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 28fe8db..408f301 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -135,6 +135,21 @@
}
@Test
+ fun enableEffect_effectIsEnabled() {
+ // Arrange: launch app and verify effect is inactive.
+ fragment.assertPreviewIsStreaming()
+ assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isFalse()
+
+ // Act: turn on effect.
+ val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+ uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+ instrumentation.waitForIdleSync()
+
+ // Assert: verify that effect is active.
+ assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isTrue()
+ }
+
+ @Test
fun controllerBound_canGetCameraControl() {
fragment.assertPreviewIsStreaming()
instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e765274..14833f1 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -16,6 +16,8 @@
package androidx.camera.integration.view;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -40,12 +42,13 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.EffectBundle;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceEffect;
import androidx.camera.core.ZoomState;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.view.CameraController;
@@ -86,6 +89,7 @@
private FrameLayout mContainer;
private Button mFlashMode;
private ToggleButton mCameraToggle;
+ private ToggleButton mEffectToggle;
private ExecutorService mExecutorService;
private ToggleButton mCaptureEnabledToggle;
private ToggleButton mAnalysisEnabledToggle;
@@ -106,6 +110,9 @@
@Nullable
private ImageAnalysis.Analyzer mWrappedAnalyzer;
+ @VisibleForTesting
+ ToneMappingSurfaceEffect mSurfaceEffect;
+
private final ImageAnalysis.Analyzer mAnalyzer = image -> {
byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
image.getPlanes()[0].getBuffer().get(bytes);
@@ -134,7 +141,7 @@
mExecutorService = Executors.newSingleThreadExecutor();
mRotationProvider = new RotationProvider(requireContext());
boolean canDetectRotation = mRotationProvider.addListener(
- CameraXExecutors.mainThreadExecutor(), mRotationListener);
+ mainThreadExecutor(), mRotationListener);
if (!canDetectRotation) {
Logger.e(TAG, "The device cannot detect rotation with motion sensor.");
}
@@ -159,6 +166,12 @@
}
});
+ // Set up post-processing effects.
+ mSurfaceEffect = new ToneMappingSurfaceEffect();
+ mEffectToggle = view.findViewById(R.id.effect_toggle);
+ mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
+ onEffectsToggled();
+
// Set up the button to change the PreviewView's size.
view.findViewById(R.id.shrink).setOnClickListener(v -> {
// Shrinks PreviewView by 10% each time it's clicked.
@@ -341,6 +354,17 @@
mExecutorService.shutdown();
}
mRotationProvider.removeListener(mRotationListener);
+ mSurfaceEffect.release();
+ }
+
+ private void onEffectsToggled() {
+ if (mEffectToggle.isChecked()) {
+ mCameraController.setEffectBundle(new EffectBundle.Builder(mainThreadExecutor())
+ .addEffect(SurfaceEffect.PREVIEW, mSurfaceEffect)
+ .build());
+ } else if (mSurfaceEffect != null) {
+ mCameraController.setEffectBundle(null);
+ }
}
void checkFailedFuture(ListenableFuture<Void> voidFuture) {
@@ -355,7 +379,7 @@
public void onFailure(@NonNull Throwable t) {
toast(t.getMessage());
}
- }, CameraXExecutors.mainThreadExecutor());
+ }, mainThreadExecutor());
}
// Synthetic access
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
new file mode 100644
index 0000000..2ed13ae
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.camera.integration.view
+
+import android.graphics.SurfaceTexture
+import android.graphics.SurfaceTexture.OnFrameAvailableListener
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.SurfaceEffect
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.checkMainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.processing.OpenGlRenderer
+import androidx.camera.core.processing.ShaderProvider
+
+/**
+ * A effect that applies tone mapping on camera output.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+class ToneMappingSurfaceEffect : SurfaceEffect, OnFrameAvailableListener {
+
+ companion object {
+ // A fragment shader that applies a yellow hue.
+ private val TONE_MAPPING_SHADER_PROVIDER = object : ShaderProvider {
+ override fun createFragmentShader(sampler: String, fragCoords: String): String {
+ return """
+ #extension GL_OES_EGL_image_external : require
+ precision mediump float;
+ uniform samplerExternalOES $sampler;
+ varying vec2 $fragCoords;
+ void main() {
+ vec4 sampleColor = texture2D($sampler, $fragCoords);
+ gl_FragColor = vec4(
+ sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+ sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+ sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+ 1.0);
+ }
+ """
+ }
+ }
+ }
+
+ private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+ private val glRenderer: OpenGlRenderer = OpenGlRenderer()
+ private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
+ private val textureTransform: FloatArray = FloatArray(16)
+ private val surfaceTransform: FloatArray = FloatArray(16)
+ private var isReleased = false
+
+ // For testing.
+ private var surfaceRequested = false
+ // For testing.
+ private var outputSurfaceProvided = false
+
+ init {
+ mainThreadExecutor().execute {
+ glRenderer.init(TONE_MAPPING_SHADER_PROVIDER)
+ }
+ }
+
+ override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+ checkMainThread()
+ if (isReleased) {
+ surfaceRequest.willNotProvideSurface()
+ return
+ }
+ surfaceRequested = true
+ val surfaceTexture = SurfaceTexture(glRenderer.textureName)
+ surfaceTexture.setDefaultBufferSize(
+ surfaceRequest.resolution.width, surfaceRequest.resolution.height
+ )
+ val surface = Surface(surfaceTexture)
+ surfaceRequest.provideSurface(surface, mainThreadExecutor()) {
+ surfaceTexture.setOnFrameAvailableListener(null)
+ surfaceTexture.release()
+ surface.release()
+ }
+ surfaceTexture.setOnFrameAvailableListener(this, mainThreadHandler)
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ checkMainThread()
+ outputSurfaceProvided = true
+ if (isReleased) {
+ surfaceOutput.close()
+ return
+ }
+ outputSurfaces[surfaceOutput] = surfaceOutput.getSurface(mainThreadExecutor()) {
+ surfaceOutput.close()
+ outputSurfaces.remove(surfaceOutput)
+ }
+ }
+
+ @VisibleForTesting
+ fun isSurfaceRequestedAndProvided(): Boolean {
+ return surfaceRequested && outputSurfaceProvided
+ }
+
+ fun release() {
+ checkMainThread()
+ if (isReleased) {
+ return
+ }
+ glRenderer.release()
+ isReleased = true
+ }
+
+ override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
+ checkMainThread()
+ if (isReleased) {
+ return
+ }
+ surfaceTexture.updateTexImage()
+ surfaceTexture.getTransformMatrix(textureTransform)
+ for (entry in outputSurfaces.entries.iterator()) {
+ val surface = entry.value
+ val surfaceOutput = entry.key
+ glRenderer.setOutputSurface(surface)
+ surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
+ glRenderer.render(surfaceTexture.timestamp, surfaceTransform)
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
index 1bce16f..47fad34 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
@@ -51,6 +51,12 @@
android:layout_height="wrap_content"
android:textOff="@string/toggle_camera_front"
android:textOn="@string/toggle_camera_back" />
+ <ToggleButton
+ android:id="@+id/effect_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/toggle_effect_off"
+ android:textOn="@string/toggle_effect_on" />
</LinearLayout>
<LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
index 9b97b3f..e7680b2 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
@@ -48,6 +48,12 @@
android:layout_height="wrap_content"
android:textOff="@string/toggle_camera_front"
android:textOn="@string/toggle_camera_back" />
+ <ToggleButton
+ android:id="@+id/effect_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/toggle_effect_off"
+ android:textOn="@string/toggle_effect_on" />
</LinearLayout>
<LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
index d4d6509..01ea7da 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -36,6 +36,8 @@
<string name="toggle_analyzer_not_set">Analyzer not set</string>
<string name="toggle_camera_front">Front</string>
<string name="toggle_camera_back">Back</string>
+ <string name="toggle_effect_on">Effect On</string>
+ <string name="toggle_effect_off">Effect Off</string>
<string name="btn_remove_or_add">Remove/Add</string>
<string name="btn_shrink">Shrink</string>
<string name="btn_switch">Switch</string>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 6be5b38..ca257d7 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -984,4 +984,93 @@
@Composable fun Text(s: String) {}
"""
)
+
+ @Test
+ fun memoizeLambdaInsideFunctionReturningValue() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Test(foo: Foo): Int =
+ Consume { foo.value }
+ """,
+ """
+ @Composable
+ fun Test(foo: Foo, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(Test)<{>,<Consum...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = Consume(remember(foo, {
+ {
+ foo.value
+ }
+ }, %composer, 0b1110 and %changed), %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+ }
+
+ """.trimIndent(),
+ """
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.Stable
+
+ @Composable
+ fun Consume(block: () -> Int): Int = block()
+
+ @Stable
+ class Foo {
+ val value: Int = 0
+ }
+ """.trimIndent()
+ )
+
+ @Test
+ fun testComposableCaptureInDelegates() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.*
+
+ class Test(val value: Int) : Delegate by Impl({
+ value
+ })
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ class Test(val value: Int) : Delegate {
+ private val %%delegate_0: Impl = Impl(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ value
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ )
+ val content: Function2<Composer, Int, Unit>
+ get() {
+ return <this>.%%delegate_0.content
+ }
+ static val %stable: Int = 0
+ }
+ """,
+ """
+ import androidx.compose.runtime.Composable
+
+ interface Delegate {
+ val content: @Composable () -> Unit
+ }
+
+ class Impl(override val content: @Composable () -> Unit) : Delegate
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 53460d2..c642dff 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -82,7 +82,7 @@
7100 to "1.2.0-rc01",
7101 to "1.2.0-rc02",
7102 to "1.2.0-rc03",
- 7103 to "1.2.0-rc04",
+ 7103 to "1.2.0",
8000 to "1.3.0-alpha01",
8100 to "1.3.0-alpha02",
)
@@ -97,7 +97,7 @@
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- const val compilerVersion: String = "1.3.0-beta01"
+ const val compilerVersion: String = "1.3.0-rc01"
private val minimumRuntimeVersion: String
get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index f47eedb..c3410b9 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -259,15 +259,16 @@
override fun recordCapture(local: IrValueDeclaration?): Boolean {
val isThis = local == thisParam
val isCtorParam = (local?.parent as? IrConstructor)?.parent === declaration
- if (local != null && collectors.isNotEmpty() && isThis) {
+ val isClassParam = isThis || isCtorParam
+ if (local != null && collectors.isNotEmpty() && isClassParam) {
for (collector in collectors) {
collector.recordCapture(local)
}
}
- if (local != null && declaration.isLocal && !isThis && !isCtorParam) {
+ if (local != null && declaration.isLocal && !isClassParam) {
captures.add(local)
}
- return isThis || isCtorParam
+ return isClassParam
}
override fun recordCapture(local: IrSymbolOwner?) { }
override fun pushCollector(collector: CaptureCollector) {
@@ -415,10 +416,7 @@
val composable = declaration.allowsComposableCalls
val canRemember = composable &&
// Don't use remember in an inline function
- !descriptor.isInline &&
- // Don't use remember if in a composable that returns a value
- // TODO(b/150390108): Consider allowing remember in effects
- descriptor.returnType.let { it != null && it.isUnit() }
+ !descriptor.isInline
val context = FunctionContext(declaration, composable, canRemember)
declarationContextStack.push(context)
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,7 +1,5 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
- Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
- Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
- Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -36,6 +36,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -47,6 +48,7 @@
}
public final class ClipScrollableContainerKt {
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -231,6 +233,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -446,7 +449,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -581,7 +584,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
public final class IntervalListKt {
}
+ public final class LazyLayoutItemProviderKt {
+ }
+
public final class LazyLayoutKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 27223c7..091d626 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -37,7 +37,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -51,7 +51,7 @@
}
public final class ClipScrollableContainerKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -285,7 +285,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
- method @androidx.compose.foundation.ExperimentalFoundationApi public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -503,7 +503,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -639,7 +639,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -731,7 +731,7 @@
package androidx.compose.foundation.lazy.layout {
@androidx.compose.foundation.ExperimentalFoundationApi public sealed interface IntervalList<T> {
- method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+ method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
method public operator androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
method public int getSize();
property public abstract int size;
@@ -749,6 +749,13 @@
public final class IntervalListKt {
}
+ @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyLayoutIntervalContent {
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? getKey();
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> getType();
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? key;
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> type;
+ }
+
@androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
method @androidx.compose.runtime.Composable public void Item(int index);
method public default Object? getContentType(int index);
@@ -759,6 +766,12 @@
property public default java.util.Map<java.lang.Object,java.lang.Integer> keyToIndexMap;
}
+ public final class LazyLayoutItemProviderKt {
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider DelegatingLazyLayoutItemProvider(androidx.compose.runtime.State<? extends androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider> delegate);
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static <T extends androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent> androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider LazyLayoutItemProvider(androidx.compose.foundation.lazy.layout.IntervalList<? extends T> intervals, kotlin.ranges.IntRange nearestItemsRange, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> itemContent);
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static Object! getDefaultLazyLayoutKey(int index);
+ }
+
public final class LazyLayoutKt {
method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayout(androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider itemProvider, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState? prefetchState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
}
@@ -788,6 +801,7 @@
}
public final class LazyNearestItemsRangeKt {
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
}
public final class Lazy_androidKt {
@@ -797,7 +811,7 @@
@androidx.compose.foundation.ExperimentalFoundationApi public final class MutableIntervalList<T> implements androidx.compose.foundation.lazy.layout.IntervalList<T> {
ctor public MutableIntervalList();
method public void addInterval(int size, T? value);
- method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+ method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
method public androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
method public int getSize();
property public int size;
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,7 +1,5 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
- Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
- Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
- Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -36,6 +36,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -47,6 +48,7 @@
}
public final class ClipScrollableContainerKt {
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -231,6 +233,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -446,7 +449,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -581,7 +584,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
public final class IntervalListKt {
}
+ public final class LazyLayoutItemProviderKt {
+ }
+
public final class LazyLayoutKt {
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
index 0c76fe5..0750a5c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
@@ -27,7 +27,6 @@
* @param constraints [Constraints] used to measure the scrollable container
* @param orientation orientation of the scrolling
*/
-@ExperimentalFoundationApi
fun checkScrollableContainerConstraints(
constraints: Constraints,
orientation: Orientation
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
index 8add015..b779125 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
@@ -33,7 +33,6 @@
*
* @param orientation orientation of the scrolling
*/
-@ExperimentalFoundationApi
fun Modifier.clipScrollableContainer(orientation: Orientation) =
then(
if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 3428f11..8b10b1a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -321,7 +321,6 @@
val isVertical: Boolean,
val overscrollEffect: OverscrollEffect
) : LayoutModifier {
- @OptIn(ExperimentalFoundationApi::class)
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index c882690..02e20cc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -224,7 +224,6 @@
*
* @return `true` if scroll direction should be reversed, `false` otherwise.
*/
- @ExperimentalFoundationApi
fun reverseDirection(
layoutDirection: LayoutDirection,
orientation: Orientation,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index b918123..de9fa2e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -74,7 +74,7 @@
content: LazyListScope.() -> Unit
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyListItemProvider(state, content)
val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 75d380a..8c28355 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -17,7 +17,14 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@ExperimentalFoundationApi
internal interface LazyListItemProvider : LazyLayoutItemProvider {
@@ -26,3 +33,59 @@
/** The scope used by the item content lambdas */
val itemScope: LazyItemScopeImpl
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyListItemProvider(
+ state: LazyListState,
+ content: LazyListScope.() -> Unit
+): LazyListItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = remember(state) { { state.firstVisibleItemIndex } },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemProviderState = derivedStateOf {
+ val listScope = LazyListScopeImpl().apply(latestContent.value)
+ LazyListItemProviderImpl(
+ listScope.intervals,
+ nearestItemsRangeState.value,
+ listScope.headerIndexes,
+ )
+ }
+ object : LazyListItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val headerIndexes: List<Int> get() = itemProviderState.value.headerIndexes
+ override val itemScope: LazyItemScopeImpl get() = itemProviderState.value.itemScope
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyListItemProviderImpl(
+ intervals: IntervalList<LazyListIntervalContent>,
+ nearestItemsRange: IntRange,
+ override val headerIndexes: List<Int>,
+ override val itemScope: LazyItemScopeImpl = LazyItemScopeImpl()
+) : LazyListItemProvider,
+ LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval: LazyListIntervalContent, index: Int ->
+ interval.item.invoke(itemScope, index)
+ }
+ )
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
deleted file mode 100644
index 2229a03..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright 2021 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.compose.foundation.lazy
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
-
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
- state: LazyListState,
- content: LazyListScope.() -> Unit
-): LazyListItemProvider {
- val latestContent = rememberUpdatedState(content)
-
- val nearestItemsRangeState = remember(state) {
- derivedStateOf(structuralEqualityPolicy()) {
- calculateNearestItemsRange(
- slidingWindowSize = NearestItemsSlidingWindowSize,
- extraItemCount = NearestItemsExtraItemCount,
- firstVisibleItem = state.firstVisibleItemIndex
- )
- }
- }
-
- return remember(nearestItemsRangeState) {
- LazyListItemProviderImpl(
- derivedStateOf {
- val listScope = LazyListScopeImpl().apply(latestContent.value)
- LazyListItemsSnapshot(
- listScope.intervals,
- listScope.headerIndexes,
- nearestItemsRangeState.value
- )
- }
- )
- }
-}
-
-@ExperimentalFoundationApi
-internal class LazyListItemsSnapshot(
- private val intervals: IntervalList<LazyListIntervalContent>,
- val headerIndexes: List<Int>,
- nearestItemsRange: IntRange
-) {
- val itemsCount get() = intervals.size
-
- fun getKey(index: Int): Any {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- val key = interval.value.key?.invoke(localIntervalIndex)
- return key ?: getDefaultLazyLayoutKey(index)
- }
-
- @Composable
- fun Item(scope: LazyItemScope, index: Int) {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- interval.value.item.invoke(scope, localIntervalIndex)
- }
-
- val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
- fun getContentType(index: Int): Any? {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.type.invoke(localIntervalIndex)
- }
-}
-
-@ExperimentalFoundationApi
-internal class LazyListItemProviderImpl(
- private val itemsSnapshot: State<LazyListItemsSnapshot>
-) : LazyListItemProvider {
-
- override val itemScope = LazyItemScopeImpl()
-
- override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
-
- override val itemCount get() = itemsSnapshot.value.itemsCount
-
- override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
- @Composable
- override fun Item(index: Int) {
- itemsSnapshot.value.Item(itemScope, index)
- }
-
- override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
- override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
- range: IntRange,
- list: IntervalList<LazyListIntervalContent>
-): Map<Any, Int> {
- val first = range.first
- check(first >= 0)
- val last = minOf(range.last, list.size - 1)
- return if (last < first) {
- emptyMap()
- } else {
- hashMapOf<Any, Int>().also { map ->
- list.forEach(
- fromIndex = first,
- toIndex = last,
- ) {
- if (it.value.key != null) {
- val keyFactory = requireNotNull(it.value.key)
- val start = maxOf(first, it.startIndex)
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- map[keyFactory(i - it.startIndex)] = i
- }
- }
- }
- }
- }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 30
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private const val NearestItemsExtraItemCount = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
index 4ea8125..ef1c2f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -72,8 +73,9 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
internal class LazyListIntervalContent(
- val key: ((index: Int) -> Any)?,
- val type: ((index: Int) -> Any?),
+ override val key: ((index: Int) -> Any)?,
+ override val type: ((index: Int) -> Any?),
val item: @Composable LazyItemScope.(index: Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
index d785a83..719d441 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemProvider::getKey
var result = -1
for (index in 0 until itemProvider.itemCount) {
- if (key(index) == needle) {
+ if (itemProvider.getKey(index) == needle) {
result = index
break
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 16f698d..a114e0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -75,7 +75,7 @@
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyGridItemProvider(state, content)
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index df4d695..4b27068 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,9 +17,94 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@ExperimentalFoundationApi
internal interface LazyGridItemProvider : LazyLayoutItemProvider {
val spanLayoutProvider: LazyGridSpanLayoutProvider
+ val hasCustomSpans: Boolean
+
+ fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyGridItemProvider(
+ state: LazyGridState,
+ content: LazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = remember(state) {
+ { state.firstVisibleItemIndex }
+ },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemProviderState: State<LazyGridItemProvider> = derivedStateOf {
+ val gridScope = LazyGridScopeImpl().apply(latestContent.value)
+ LazyGridItemProviderImpl(
+ gridScope.intervals,
+ gridScope.hasCustomSpans,
+ nearestItemsRangeState.value
+ )
+ }
+
+ object : LazyGridItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider
+ get() = itemProviderState.value.spanLayoutProvider
+
+ override val hasCustomSpans: Boolean
+ get() = itemProviderState.value.hasCustomSpans
+
+ override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan =
+ with(itemProviderState.value) {
+ getSpan(index)
+ }
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyGridItemProviderImpl(
+ private val intervals: IntervalList<LazyGridIntervalContent>,
+ override val hasCustomSpans: Boolean,
+ nearestItemsRange: IntRange
+) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval, index ->
+ interval.item.invoke(LazyGridItemScopeImpl, index)
+ }
+) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider =
+ LazyGridSpanLayoutProvider(this)
+
+ override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
+ val interval = intervals[index]
+ val localIntervalIndex = index - interval.startIndex
+ return interval.value.span.invoke(this, localIntervalIndex)
+ }
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
deleted file mode 100644
index 9c2d01e..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2021 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.compose.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
-
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
- state: LazyGridState,
- content: LazyGridScope.() -> Unit,
-): LazyGridItemProvider {
- val latestContent = rememberUpdatedState(content)
- val nearestItemsRangeState = remember(state) {
- derivedStateOf(structuralEqualityPolicy()) {
- calculateNearestItemsRange(
- slidingWindowSize = NearestItemsSlidingWindowSize,
- extraItemCount = NearestItemsExtraItemCount,
- firstVisibleItem = state.firstVisibleItemIndex
- )
- }
- }
-
- return remember(nearestItemsRangeState) {
- LazyGridItemProviderImpl(
- derivedStateOf {
- val listScope = LazyGridScopeImpl().apply(latestContent.value)
- LazyGridItemsSnapshot(
- listScope.intervals,
- listScope.hasCustomSpans,
- nearestItemsRangeState.value
- )
- }
- )
- }
-}
-
-@ExperimentalFoundationApi
-internal class LazyGridItemsSnapshot(
- private val intervals: IntervalList<LazyGridIntervalContent>,
- val hasCustomSpans: Boolean,
- nearestItemsRange: IntRange
-) {
- val itemsCount get() = intervals.size
-
- val spanLayoutProvider = LazyGridSpanLayoutProvider(this)
-
- fun getKey(index: Int): Any {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- val key = interval.value.key?.invoke(localIntervalIndex)
- return key ?: getDefaultLazyLayoutKey(index)
- }
-
- fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.span.invoke(this, localIntervalIndex)
- }
-
- @Composable
- fun Item(index: Int) {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- interval.value.item.invoke(LazyGridItemScopeImpl, localIntervalIndex)
- }
-
- val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
- fun getContentType(index: Int): Any? {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.type.invoke(localIntervalIndex)
- }
-}
-
-@ExperimentalFoundationApi
-internal class LazyGridItemProviderImpl(
- private val itemsSnapshot: State<LazyGridItemsSnapshot>
-) : LazyGridItemProvider {
- override val itemCount get() = itemsSnapshot.value.itemsCount
-
- override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
- @Composable
- override fun Item(index: Int) {
- itemsSnapshot.value.Item(index)
- }
-
- override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
- override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-
- override val spanLayoutProvider: LazyGridSpanLayoutProvider
- get() = itemsSnapshot.value.spanLayoutProvider
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
- range: IntRange,
- list: IntervalList<LazyGridIntervalContent>
-): Map<Any, Int> {
- val first = range.first
- check(first >= 0)
- val last = minOf(range.last, list.size - 1)
- return if (last < first) {
- emptyMap()
- } else {
- hashMapOf<Any, Int>().also { map ->
- list.forEach(
- fromIndex = first,
- toIndex = last,
- ) {
- if (it.value.key != null) {
- val keyFactory = requireNotNull(it.value.key)
- val start = maxOf(first, it.startIndex)
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- map[keyFactory(i - it.startIndex)] = i
- }
- }
- }
- }
- }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 90
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private const val NearestItemsExtraItemCount = 200
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
index 281996a..e05ff92 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -67,8 +68,8 @@
@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridIntervalContent(
- val key: ((index: Int) -> Any)?,
+ override val key: ((index: Int) -> Any)?,
val span: LazyGridItemSpanScope.(Int) -> GridItemSpan,
- val type: ((index: Int) -> Any?),
+ override val type: ((index: Int) -> Any?),
val item: @Composable LazyGridItemScope.(Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index 18b32e7..6c0f594 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -21,7 +21,7 @@
import kotlin.math.sqrt
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
class LineConfiguration(val firstItemIndex: Int, val spans: List<GridItemSpan>)
/** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -60,7 +60,7 @@
List(currentSlotsPerLine) { GridItemSpan(1) }.also { previousDefaultSpans = it }
}
- val totalSize get() = itemsSnapshot.itemsCount
+ val totalSize get() = itemProvider.itemCount
/** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
var slotsPerLine = 0
@@ -72,7 +72,7 @@
}
fun getLineConfiguration(lineIndex: Int): LineConfiguration {
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
// Quick return when all spans are 1x1 - in this case we can easily calculate positions.
val firstItemIndex = lineIndex * slotsPerLine
return LineConfiguration(
@@ -172,7 +172,7 @@
return LineIndex(0)
}
require(itemIndex < totalSize)
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
return LineIndex(itemIndex / slotsPerLine)
}
@@ -210,7 +210,7 @@
return LineIndex(currentLine)
}
- private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+ private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
with(LazyGridItemSpanScopeImpl) {
maxCurrentLineSpan = maxSpan
maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 54cafcd..b331a9e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemProvider::getKey
var result = -1
for (index in 0 until itemProvider.itemCount) {
- if (key(index) == needle) {
+ if (itemProvider.getKey(index) == needle) {
result = index
break
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
index f855bea..0a6ca89 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
@@ -31,7 +31,7 @@
* @param T type of values each interval contains in [Interval.value].
*/
@ExperimentalFoundationApi
-sealed interface IntervalList<T> {
+sealed interface IntervalList<out T> {
/**
* The total amount of items in all the intervals.
@@ -69,7 +69,7 @@
*
* @see get
*/
- class Interval<T> internal constructor(
+ class Interval<out T> internal constructor(
/**
* The index of the first item in the interval.
*/
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
index 9534b0e..828bdfe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
/**
* Provides all the needed info about the items which could be later composed and displayed as
@@ -67,4 +68,138 @@
* 3) This objects can't be equals to any object which could be provided by a user as a custom key.
*/
@ExperimentalFoundationApi
+@Suppress("MissingNullability")
expect fun getDefaultLazyLayoutKey(index: Int): Any
+
+/**
+ * Common content holder to back interval-based `item` DSL of lazy layouts.
+ */
+@ExperimentalFoundationApi
+interface LazyLayoutIntervalContent {
+ /**
+ * Returns item key based on a local index for the current interval.
+ */
+ val key: ((index: Int) -> Any)? get() = null
+
+ /**
+ * Returns item type based on a local index for the current interval.
+ */
+ val type: ((index: Int) -> Any?) get() = { null }
+}
+
+/**
+ * Default implementation of [LazyLayoutItemProvider] shared by lazy layout implementations.
+ *
+ * @param intervals [IntervalList] of [LazyLayoutIntervalContent] defined by lazy list DSL
+ * @param nearestItemsRange range of indices considered near current viewport
+ * @param itemContent composable content based on index inside provided interval
+ */
+@ExperimentalFoundationApi
+fun <T : LazyLayoutIntervalContent> LazyLayoutItemProvider(
+ intervals: IntervalList<T>,
+ nearestItemsRange: IntRange,
+ itemContent: @Composable (interval: T, index: Int) -> Unit,
+): LazyLayoutItemProvider =
+ DefaultLazyLayoutItemsProvider(itemContent, intervals, nearestItemsRange)
+
+@ExperimentalFoundationApi
+private class DefaultLazyLayoutItemsProvider<IntervalContent : LazyLayoutIntervalContent>(
+ val itemContentProvider: @Composable IntervalContent.(index: Int) -> Unit,
+ val intervals: IntervalList<IntervalContent>,
+ nearestItemsRange: IntRange
+) : LazyLayoutItemProvider {
+ override val itemCount get() = intervals.size
+
+ override val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
+
+ @Composable
+ override fun Item(index: Int) {
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.itemContentProvider(localIndex)
+ }
+ }
+
+ override fun getKey(index: Int): Any =
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.key?.invoke(localIndex) ?: getDefaultLazyLayoutKey(index)
+ }
+
+ override fun getContentType(index: Int): Any? =
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.type.invoke(localIndex)
+ }
+
+ private inline fun <T> withLocalIntervalIndex(
+ index: Int,
+ block: (localIndex: Int, content: IntervalContent) -> T
+ ): T {
+ val interval = intervals[index]
+ val localIntervalIndex = index - interval.startIndex
+ return block(localIntervalIndex, interval.value)
+ }
+
+ /**
+ * Traverses the interval [list] in order to create a mapping from the key to the index for all
+ * the indexes in the passed [range].
+ * The returned map will not contain the values for intervals with no key mapping provided.
+ */
+ @ExperimentalFoundationApi
+ private fun generateKeyToIndexMap(
+ range: IntRange,
+ list: IntervalList<LazyLayoutIntervalContent>
+ ): Map<Any, Int> {
+ val first = range.first
+ check(first >= 0)
+ val last = minOf(range.last, list.size - 1)
+ return if (last < first) {
+ emptyMap()
+ } else {
+ hashMapOf<Any, Int>().also { map ->
+ list.forEach(
+ fromIndex = first,
+ toIndex = last,
+ ) {
+ if (it.value.key != null) {
+ val keyFactory = requireNotNull(it.value.key)
+ val start = maxOf(first, it.startIndex)
+ val end = minOf(last, it.startIndex + it.size - 1)
+ for (i in start..end) {
+ map[keyFactory(i - it.startIndex)] = i
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Delegating version of [LazyLayoutItemProvider], abstracting internal [State] access.
+ * This way, passing [LazyLayoutItemProvider] will not trigger recomposition unless
+ * its methods are called within composable functions.
+ *
+ * @param delegate [State] to delegate [LazyLayoutItemProvider] functionality to.
+ */
+@ExperimentalFoundationApi
+fun DelegatingLazyLayoutItemProvider(
+ delegate: State<LazyLayoutItemProvider>
+): LazyLayoutItemProvider =
+ DefaultDelegatingLazyLayoutItemProvider(delegate)
+
+@ExperimentalFoundationApi
+private class DefaultDelegatingLazyLayoutItemProvider(
+ private val delegate: State<LazyLayoutItemProvider>
+) : LazyLayoutItemProvider {
+ override val itemCount: Int get() = delegate.value.itemCount
+
+ @Composable
+ override fun Item(index: Int) {
+ delegate.value.Item(index)
+ }
+
+ override val keyToIndexMap: Map<Any, Int> get() = delegate.value.keyToIndexMap
+
+ override fun getKey(index: Int): Any = delegate.value.getKey(index)
+
+ override fun getContentType(index: Int): Any? = delegate.value.getContentType(index)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
index f34ae9f..c602a6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
@@ -16,19 +16,48 @@
package androidx.compose.foundation.lazy.layout
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.structuralEqualityPolicy
+
+/**
+ * Calculate and memoize range of indexes which contains at least [extraItemCount] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ *
+ * @param firstVisibleItemIndex Provider of the first item index currently visible on screen.
+ * @param slidingWindowSize Number items user can scroll up to this number of items until we have to
+ * regenerate item mapping.
+ * @param extraItemCount The minimum amount of items near the first visible item we want
+ * to have mapping for.
+ * @return range of indexes with items near current the first visible position.
+ */
+@ExperimentalFoundationApi
+@Composable
+fun rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex: () -> Int,
+ slidingWindowSize: () -> Int,
+ extraItemCount: () -> Int
+): State<IntRange> =
+ remember(firstVisibleItemIndex, slidingWindowSize, extraItemCount) {
+ derivedStateOf(structuralEqualityPolicy()) {
+ calculateNearestItemsRange(
+ firstVisibleItemIndex(),
+ slidingWindowSize(),
+ extraItemCount()
+ )
+ }
+ }
+
/**
* Returns a range of indexes which contains at least [extraItemCount] items near
* the first visible item. It is optimized to return the same range for small changes in the
- * [firstVisibleItem] value so we do not regenerate the map on each scroll.
- *
- * It uses the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- *
- * @param firstVisibleItem currently visible item
- * @param slidingWindowSize size of the sliding window for the nearest item calculation
- * @param extraItemCount minimum amount of items near the first item we want to have mapping for.
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
*/
-internal fun calculateNearestItemsRange(
+private fun calculateNearestItemsRange(
firstVisibleItem: Int,
slidingWindowSize: Int,
extraItemCount: Int
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 56e0e66..29a5f39 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -16,8 +16,10 @@
package androidx.compose.foundation.text
+import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.whenever
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -27,8 +29,14 @@
class TextControllerTest {
@Test
fun `semantics modifier recreated when TextDelegate is set`() {
- val textDelegateBefore = mock<TextDelegate>()
- val textDelegateAfter = mock<TextDelegate>()
+ val textDelegateBefore = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+ }
+
+ val textDelegateAfter = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+ }
+
// Make sure that mock doesn't do smart memory management:
assertThat(textDelegateAfter).isNotSameInstanceAs(textDelegateBefore)
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9d55522..05d810f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
@@ -68,6 +69,7 @@
private lateinit var gesture: TextDragObserver
private lateinit var layoutCoordinates: LayoutCoordinates
private lateinit var state: TextState
+ private lateinit var fontFamilyResolver: FontFamily.Resolver
@Before
fun setup() {
@@ -76,8 +78,15 @@
layoutCoordinates = mock {
on { isAttached } doReturn true
}
+ fontFamilyResolver = mock()
- state = TextState(mock(), selectableId)
+ val delegate = TextDelegate(
+ text = AnnotatedString(""),
+ style = TextStyle(),
+ density = Density(1.0f),
+ fontFamilyResolver = fontFamilyResolver
+ )
+ state = TextState(delegate, selectableId)
state.layoutCoordinates = layoutCoordinates
state.layoutResult = TextLayoutResult(
TextLayoutInput(
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
index db0d20d..6c3d32d 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
@@ -23,29 +23,30 @@
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.benchmark.perfetto.PerfettoCapture
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
@LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
/**
* End-to-end test for compose-runtime-tracing verifying that names of Composables show up in
* a Perfetto trace.
*/
-class TrivialTracingBenchmark {
+@OptIn(ExperimentalMetricApi::class)
+class TrivialTracingBenchmark(private val composableName: String) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
- @OptIn(ExperimentalMetricApi::class)
@Test
fun test_composable_names_present_in_trace() {
- val metrics = COMPOSABLE_NAMES.map { composableName ->
+ val metrics = listOf(
TraceSectionMetric("%$PACKAGE_NAME.$composableName %$FILE_NAME:%")
- }
+ )
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = metrics,
@@ -74,5 +75,9 @@
"Bar_4888EA32_ABC5_4550_BA78_1247FEC1AAC9",
"Baz_609801AB_F5A9_47C3_9405_2E82542F21B8"
)
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun parameters() = COMPOSABLE_NAMES
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index 35f5b24..ac4def2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.material.textfield
import android.os.Build
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -38,7 +37,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -82,15 +80,13 @@
@Test
fun outlinedTextField_withInput() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput")
@@ -99,14 +95,12 @@
@Test
fun outlinedTextField_notFocused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_not_focused")
@@ -115,14 +109,12 @@
@Test
fun outlinedTextField_focused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -134,14 +126,12 @@
fun outlinedTextField_focused_rtl() {
rule.setMaterialContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
}
@@ -153,16 +143,14 @@
@Test
fun outlinedTextField_error_focused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -173,15 +161,13 @@
@Test
fun outlinedTextField_error_notFocused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -374,15 +360,13 @@
@Test
fun outlinedTextField_disabled() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled")
@@ -391,15 +375,13 @@
@Test
fun outlinedTextField_disabled_notFocusable() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -410,15 +392,13 @@
@Test
fun outlinedTextField_disabled_notScrolled() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = longText,
- onValueChange = { },
- singleLine = true,
- modifier = Modifier.requiredWidth(300.dp),
- enabled = false
- )
- }
+ OutlinedTextField(
+ value = longText,
+ onValueChange = { },
+ singleLine = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+ enabled = false
+ )
}
rule.mainClock.autoAdvance = false
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 197954f..0cbdd15 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -160,7 +161,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
@@ -304,7 +309,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index fd3c767..e9d5bf1 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -210,9 +210,9 @@
public final class ChipKt {
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
index 22c5d76..2837fd9 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
@@ -93,12 +93,16 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ null
}
)
}
@@ -112,12 +116,16 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ null
}
)
}
@@ -131,19 +139,22 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- leadingIcon = {
- Icon(
- imageVector = Icons.Filled.Home,
- contentDescription = "Localized description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ {
+ Icon(
+ imageVector = Icons.Filled.Home,
+ contentDescription = "Localized description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
}
)
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
index e1d448a..2df1372 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
@@ -20,7 +20,6 @@
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
-import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.testutils.assertAgainstGolden
@@ -329,7 +328,7 @@
onClick = {},
label = { Text("Filter Chip") },
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
@@ -342,58 +341,6 @@
}
@Test
- fun filterChip_flat_withLeadingIcon_selected_lightTheme() {
- rule.setMaterialContent(lightColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_lightTheme")
- }
-
- @Test
- fun filterChip_flat_withLeadingIcon_selected_darkTheme() {
- rule.setMaterialContent(darkColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_darkTheme")
- }
-
- @Test
fun filterChip_flat_notSelected() {
rule.setMaterialContent(lightColorScheme()) {
FilterChip(
@@ -415,7 +362,7 @@
label = { Text("Filter Chip") },
enabled = false,
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
tint = LocalContentColor.current,
@@ -429,33 +376,6 @@
}
@Test
- fun filterChip_flat_withLeadingIcon_disabled_selected() {
- rule.setMaterialContent(lightColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- enabled = false,
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_disabled_selected")
- }
-
- @Test
fun filterChip_flat_disabled_notSelected() {
rule.setMaterialContent(lightColorScheme()) {
FilterChip(
@@ -477,7 +397,7 @@
onClick = {},
label = { Text("Filter Chip") },
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
index 1a173e3..2dd3963 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
@@ -360,13 +360,6 @@
"Filter chip",
Modifier.testTag(TestChipTag)
)
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
})
}
@@ -395,7 +388,7 @@
Modifier.testTag(TestChipTag)
)
},
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
index 8a49d2f..cccb678 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.material3
import android.os.Build
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -33,7 +32,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -78,15 +76,13 @@
@Test
fun outlinedTextField_withInput() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput")
@@ -95,14 +91,12 @@
@Test
fun outlinedTextField_notFocused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_not_focused")
@@ -111,14 +105,12 @@
@Test
fun outlinedTextField_focused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -130,14 +122,12 @@
fun outlinedTextField_focused_rtl() {
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
}
@@ -149,16 +139,14 @@
@Test
fun outlinedTextField_error_focused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -169,15 +157,13 @@
@Test
fun outlinedTextField_error_notFocused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -391,15 +377,13 @@
@Test
fun outlinedTextField_disabled() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled")
@@ -408,15 +392,13 @@
@Test
fun outlinedTextField_disabled_notFocusable() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -427,15 +409,13 @@
@Test
fun outlinedTextField_disabled_notScrolled() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = longText,
- onValueChange = { },
- singleLine = true,
- modifier = Modifier.requiredWidth(300.dp),
- enabled = false
- )
- }
+ OutlinedTextField(
+ value = longText,
+ onValueChange = { },
+ singleLine = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+ enabled = false
+ )
}
rule.mainClock.autoAdvance = false
@@ -589,15 +569,13 @@
@Test
fun outlinedTextField_withInput_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput_dark")
@@ -606,14 +584,12 @@
@Test
fun outlinedTextField_focused_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -624,16 +600,14 @@
@Test
fun outlinedTextField_error_focused_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -644,15 +618,13 @@
@Test
fun outlinedTextField_disabled_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled_dark")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 3ab2181..71ae1a7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -220,8 +220,8 @@
* This filter chip is applied with a flat style. If you want an elevated style, use the
* [ElevatedFilterChip].
*
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
*
* Example of a flat FilterChip with a trailing icon:
* @sample androidx.compose.material3.samples.FilterChipSample
@@ -236,9 +236,9 @@
* @param enabled controls the enabled state of this chip. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
* @param trailingIcon optional icon at the end of the chip
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -263,7 +263,6 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
- selectedIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
@@ -277,7 +276,7 @@
enabled = enabled,
label = label,
labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
- leadingIcon = if (selected) selectedIcon else leadingIcon,
+ leadingIcon = leadingIcon,
avatar = null,
trailingIcon = trailingIcon,
elevation = elevation,
@@ -304,8 +303,8 @@
* This filter chip is applied with an elevated style. If you want a flat style, use the
* [FilterChip].
*
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
*
* Example of an elevated FilterChip with a trailing icon:
* @sample androidx.compose.material3.samples.ElevatedFilterChipSample
@@ -317,9 +316,9 @@
* @param enabled controls the enabled state of this chip. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
* @param trailingIcon optional icon at the end of the chip
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -344,7 +343,6 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
- selectedIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: SelectableChipElevation? = FilterChipDefaults.elevatedFilterChipElevation(),
@@ -358,7 +356,7 @@
enabled = enabled,
label = label,
labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
- leadingIcon = if (selected) selectedIcon else leadingIcon,
+ leadingIcon = leadingIcon,
avatar = null,
trailingIcon = trailingIcon,
elevation = elevation,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 3e59495..66c86c1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -161,7 +162,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
@@ -306,7 +311,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index cbc5577..489920f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -452,7 +452,8 @@
previousSnapshot = currentSnapshot as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = writeObserver,
- mergeParentObservers = true
+ mergeParentObservers = true,
+ ownsPreviousSnapshot = false
)
else if (readObserver == null) return block()
else currentSnapshot.takeNestedSnapshot(readObserver)
@@ -1434,7 +1435,8 @@
private val previousSnapshot: MutableSnapshot?,
internal val specifiedReadObserver: ((Any) -> Unit)?,
internal val specifiedWriteObserver: ((Any) -> Unit)?,
- private val mergeParentObservers: Boolean
+ private val mergeParentObservers: Boolean,
+ private val ownsPreviousSnapshot: Boolean
) : MutableSnapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
@@ -1454,6 +1456,9 @@
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
+ if (ownsPreviousSnapshot) {
+ previousSnapshot?.dispose()
+ }
}
override var id: Int
@@ -1486,7 +1491,8 @@
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
- readObserver = readObserver
+ readObserver = mergedReadObserver,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1508,7 +1514,8 @@
previousSnapshot = nestedSnapshot,
specifiedReadObserver = mergedReadObserver,
specifiedWriteObserver = mergedWriteObserver,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedMutableSnapshot(
@@ -1532,7 +1539,8 @@
internal class TransparentObserverSnapshot(
private val previousSnapshot: Snapshot?,
specifiedReadObserver: ((Any) -> Unit)?,
- private val mergeParentObservers: Boolean
+ private val mergeParentObservers: Boolean,
+ private val ownsPreviousSnapshot: Boolean
) : Snapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
@@ -1552,6 +1560,9 @@
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
+ if (ownsPreviousSnapshot) {
+ previousSnapshot?.dispose()
+ }
}
override var id: Int
@@ -1580,8 +1591,9 @@
val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
- previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
- readObserver = readObserver
+ currentSnapshot.takeNestedSnapshot(null),
+ mergedReadObserver,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1599,18 +1611,21 @@
private fun createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot: Snapshot?,
readObserver: ((Any) -> Unit)? = null,
+ ownsPreviousSnapshot: Boolean = false
): Snapshot = if (previousSnapshot is MutableSnapshot || previousSnapshot == null) {
TransparentObserverMutableSnapshot(
previousSnapshot = previousSnapshot as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = null,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = ownsPreviousSnapshot
)
} else {
TransparentObserverSnapshot(
previousSnapshot = previousSnapshot,
specifiedReadObserver = readObserver,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = ownsPreviousSnapshot
)
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 8d279be..cdff066 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -936,6 +936,126 @@
}
}
+ @Test
+ fun testNestedWithinTransparentSnapshotDisposedCorrectly() {
+ val outerSnapshot = TransparentObserverSnapshot(
+ previousSnapshot = currentSnapshot(),
+ specifiedReadObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+ try {
+ innerSnapshot.enter { }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+ }
+
+ @Test
+ fun testNestedWithinTransparentMutableSnapshotDisposedCorrectly() {
+ val outerSnapshot = TransparentObserverMutableSnapshot(
+ previousSnapshot = currentSnapshot() as? MutableSnapshot,
+ specifiedReadObserver = null,
+ specifiedWriteObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+ try {
+ innerSnapshot.enter { }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+ }
+
+ @Test
+ fun testTransparentSnapshotMergedWithNestedReadObserver() {
+ var outerChanges = 0
+ var innerChanges = 0
+ val state by mutableStateOf(0)
+
+ val outerSnapshot = TransparentObserverSnapshot(
+ previousSnapshot = currentSnapshot(),
+ specifiedReadObserver = { outerChanges++ },
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+ readObserver = { innerChanges++ }
+ )
+
+ try {
+ innerSnapshot.enter {
+ state // read
+ }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+
+ assertEquals(1, outerChanges)
+ assertEquals(1, innerChanges)
+ }
+
+ @Test
+ fun testTransparentMutableSnapshotMergedWithNestedReadObserver() {
+ var outerChanges = 0
+ var innerChanges = 0
+ val state by mutableStateOf(0)
+
+ val outerSnapshot = TransparentObserverMutableSnapshot(
+ previousSnapshot = currentSnapshot() as? MutableSnapshot,
+ specifiedReadObserver = { outerChanges++ },
+ specifiedWriteObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+ readObserver = { innerChanges++ }
+ )
+
+ try {
+ innerSnapshot.enter {
+ state // read
+ }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+
+ assertEquals(1, outerChanges)
+ assertEquals(1, innerChanges)
+ }
+
private var count = 0
@BeforeTest
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
index f5dcd6c..b98fc6fdd 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
@@ -42,5 +42,7 @@
.isEqualTo(LambdaLocation("TestLambdas.kt", 29, 30))
assertThat(LambdaLocation.resolve(TestLambdas.inlinedParameter))
.isEqualTo(LambdaLocation("TestLambdas.kt", 33, 33))
+ assertThat(LambdaLocation.resolve(TestLambdas.unnamed))
+ .isEqualTo(LambdaLocation("TestLambdas.kt", 35, 35))
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
index b127774..5ff2b27 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
@@ -32,6 +32,7 @@
val inlinedParameter = { o: IntOffset ->
o.x * 2
}
+ val unnamed: (Int, Int) -> Float = { _, _ -> 0f }
/**
* This inline function will appear at a line numbers
diff --git a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
index 1a52bfb..11e4a1a 100644
--- a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
+++ b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
@@ -169,7 +169,8 @@
InlineRange *ranges = nullptr;
for (int i=0; i<variableCount; i++) {
jvmtiLocalVariableEntry *variable = &variables[i];
- if (strncmp("$i$f$", variable->name, 5) == 0) {
+ char* name = variable->name;
+ if (name != nullptr && strncmp("$i$f$", name, 5) == 0) {
if (ranges == nullptr) {
jvmti->Allocate(sizeof(InlineRange) * (variableCount-i), (unsigned char **)&ranges);
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 62dd84e..43fea44 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -553,13 +553,10 @@
.flatMap { config -> config.map { RawParameter(it.key.name, it.value) } }
)
- node.mergedSemantics.addAll(
- modifierInfo.asSequence()
- .map { it.modifier }
- .filterIsInstance<SemanticsModifier>()
- .map { it.id }
- .flatMap { semanticsMap[it].orEmpty() }
- )
+ val mergedSemantics = semanticsMap.get(layoutInfo.semanticsId)
+ if (mergedSemantics != null) {
+ node.mergedSemantics.addAll(mergedSemantics)
+ }
node.id = modifierInfo.asSequence()
.map { it.extra }
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index ab2b6fa..79fe224 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+
+import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -21,30 +23,99 @@
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("org.jetbrains.kotlin.android")
}
-dependencies {
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
- implementation(libs.kotlinStdlib)
+if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- api "androidx.annotation:annotation:1.1.0"
+ dependencies {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block below
+ */
- api("androidx.compose.runtime:runtime:1.2.0-rc02")
- api(project(":compose:ui:ui"))
+ implementation(libs.kotlinStdlib)
- androidTestImplementation project(":compose:ui:ui-test-junit4")
+ api "androidx.annotation:annotation:1.1.0"
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
+ api("androidx.compose.runtime:runtime:1.2.0-rc02")
+ api(project(":compose:ui:ui"))
- androidTestImplementation(libs.truth)
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ androidTestImplementation project(":compose:ui:ui-test-junit4")
+
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ androidXComposeMultiplatform {
+ android()
+ desktop()
+ }
+
+ kotlin {
+
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+
+ implementation(libs.kotlinStdlib)
+
+ api "androidx.annotation:annotation:1.1.0"
+
+ api("androidx.compose.runtime:runtime:1.2.0-rc02")
+ api(project(":compose:ui:ui"))
+ }
+ jvmMain.dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ androidMain.dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test-junit"))
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest.dependencies {
+ implementation(libs.truth)
+ }
+ androidAndroidTest.dependencies {
+ implementation(project(":compose:ui:ui-test-junit4"))
+
+ implementation(libs.junit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testRules)
+
+ implementation(libs.truth)
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:material:material"))
+ implementation("androidx.activity:activity-compose:1.3.1")
+ }
+ }
+ }
+ dependencies {
+ samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
+ }
}
androidx {
diff --git a/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
diff --git a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index bf169d7..6d14189 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1871,6 +1871,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1916,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2769,9 +2772,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index f6fb19e..32bc836 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2017,6 +2017,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -2051,6 +2052,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -2062,6 +2064,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2982,9 +2985,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index f6326ba..2b300fe 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1871,6 +1871,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1916,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2805,9 +2808,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 5adc1c1..c77098c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -904,7 +904,6 @@
val nodes = SemanticsOwner(
LayoutNode().also {
it.modifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
@@ -1264,9 +1263,9 @@
mergeDescendants: Boolean,
properties: (SemanticsPropertyReceiver.() -> Unit)
): SemanticsNode {
- val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, false, properties)
+ val semanticsModifier = SemanticsModifierCore(mergeDescendants, false, properties)
return SemanticsNode(
- SemanticsEntity(InnerPlaceable(LayoutNode()), semanticsModifier),
+ SemanticsEntity(InnerPlaceable(LayoutNode(semanticsId = id)), semanticsModifier),
true
)
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index a7ea12c..0fe6b5d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
@@ -3755,6 +3756,12 @@
) {
}
+ override fun transform(matrix: Matrix) {
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ }
+
override fun mapOffset(point: Offset, inverse: Boolean) = point
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
index b75e054..4deac3c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
@@ -61,6 +61,7 @@
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -976,6 +977,38 @@
}
}
+ @Test
+ fun lookaheadLayoutTransformFrom() {
+ val matrix = Matrix()
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ LookaheadLayout(
+ measurePolicy = { measurables, constraints ->
+ val placeable = measurables[0].measure(constraints)
+ // Position the children.
+ layout(placeable.width + 10, placeable.height + 10) {
+ placeable.place(10, 10)
+ }
+ },
+ content = {
+ Box(
+ Modifier
+ .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
+ layoutCoordinates.transformFrom(
+ lookaheadScopeCoordinates,
+ matrix
+ )
+ }
+ .size(10.dp))
+ }
+ )
+ }
+ }
+ rule.waitForIdle()
+ val posInChild = matrix.map(Offset(10f, 10f))
+ assertEquals(Offset.Zero, posInChild)
+ }
+
private fun assertSameLayoutWithAndWithoutLookahead(
content: @Composable (
modifier: Modifier
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 241e5cd..86a6f47 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -406,6 +406,33 @@
shapeColor = Color.Red
)
}
+
+ @Test
+ fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+ var childModifier by mutableStateOf(Modifier as Modifier)
+
+ rule.setContent {
+ LayoutUsingIntrinsics() {
+ Box(
+ Modifier
+ .testTag("child")
+ .then(childModifier)
+ )
+ }
+ }
+
+ rule.onNodeWithTag("child")
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ childModifier = Modifier.withIntrinsics(30.dp, 20.dp)
+ }
+
+ rule.onNodeWithTag("child")
+ .assertWidthIsEqualTo(30.dp)
+ .assertHeightIsEqualTo(20.dp)
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 085a1f7..55e8f7e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -70,7 +70,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
import kotlin.math.max
@MediumTest
@@ -91,16 +90,6 @@
isDebugInspectorInfoEnabled = false
}
- private fun executeUpdateBlocking(updateFunction: () -> Unit) {
- val latch = CountDownLatch(1)
- rule.runOnUiThread {
- updateFunction()
- latch.countDown()
- }
-
- latch.await()
- }
-
@Test
fun unchangedSemanticsDoesNotCauseRelayout() {
val layoutCounter = Counter(0)
@@ -118,6 +107,22 @@
}
@Test
+ fun valueSemanticsAreEqual() {
+ assertEquals(
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ },
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ }
+ )
+ }
+
+ @Test
fun depthFirstPropertyConcat() {
val root = "root"
val child1 = "child1"
@@ -533,6 +538,12 @@
val isAfter = mutableStateOf(false)
+ val content: @Composable () -> Unit = {
+ SimpleTestLayout {
+ nodeCount++
+ }
+ }
+
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
@@ -543,12 +554,9 @@
return@onClick true
}
)
- }
- ) {
- SimpleTestLayout {
- nodeCount++
- }
- }
+ },
+ content = content
+ )
}
// This isn't the important part, just makes sure everything is behaving as expected
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9344d54..0885058 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.viewinterop
import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
@@ -33,6 +35,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@@ -45,8 +48,10 @@
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
@@ -272,7 +277,10 @@
}
}
rule.setContent {
- AndroidView({ frameLayout }, Modifier.testTag("view").background(color = Color.Blue))
+ AndroidView({ frameLayout },
+ Modifier
+ .testTag("view")
+ .background(color = Color.Blue))
}
rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
@@ -356,9 +364,11 @@
CompositionLocalProvider(LocalDensity provides density) {
AndroidView(
{ FrameLayout(it) },
- Modifier.requiredSize(size).onGloballyPositioned {
- assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
- }
+ Modifier
+ .requiredSize(size)
+ .onGloballyPositioned {
+ assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+ }
)
}
}
@@ -566,7 +576,11 @@
val sizeDp = with(rule.density) { size.toDp() }
rule.setContent {
Column {
- Box(Modifier.size(sizeDp).background(Color.Blue).testTag("box"))
+ Box(
+ Modifier
+ .size(sizeDp)
+ .background(Color.Blue)
+ .testTag("box"))
AndroidView(factory = { SurfaceView(it) })
}
}
@@ -619,6 +633,40 @@
}
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun androidView_noClip() {
+ rule.setContent {
+ Box(Modifier.fillMaxSize().background(Color.White)) {
+ with(LocalDensity.current) {
+ Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+ Box(
+ Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+ ) {
+ AndroidView(factory = { context ->
+ object : View(context) {
+ init {
+ clipToOutline = false
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ val paint = Paint()
+ paint.color = Color.Blue.toArgb()
+ paint.style = Paint.Style.FILL
+ canvas.drawRect(0f, 0f, 150f, 150f, paint)
+ }
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+ rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
+ Color.Blue
+ }
+ }
+
private class StateSavingView(
private val key: String,
private val value: String,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a60a1e2..532f0d5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -189,7 +189,6 @@
private set
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 130f682..b7ff6d8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -1558,7 +1558,8 @@
) {
val androidView = view.androidViewsHandler.layoutNodeToHolder[wrapper.layoutNode]
if (androidView == null) {
- virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.modifier.id)
+ virtualViewId =
+ semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.layoutNode.semanticsId)
}
}
}
@@ -1719,7 +1720,7 @@
?.isMergingSemanticsOfDescendants == true
}?.outerSemantics?.let { semanticsWrapper = it }
}
- val id = semanticsWrapper.modifier.id
+ val id = semanticsWrapper.layoutNode.semanticsId
if (!subtreeChangedSemanticsNodesIds.add(id)) {
return
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index a62b572..07f4b2e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
@@ -341,6 +342,17 @@
this.invalidateParentLayer = invalidateParentLayer
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(matrixCache.calculateMatrix(renderNode))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ val inverse = matrixCache.calculateInverseMatrix(renderNode)
+ if (inverse != null) {
+ matrix.timesAssign(inverse)
+ }
+ }
+
companion object {
private val getMatrix: (DeviceRenderNode, android.graphics.Matrix) -> Unit = { rn, matrix ->
rn.getMatrix(matrix)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 912ff1f..55d1dcd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
@@ -356,6 +357,17 @@
this.invalidateParentLayer = invalidateParentLayer
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(matrixCache.calculateMatrix(this))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ val inverse = matrixCache.calculateInverseMatrix(this)
+ if (inverse != null) {
+ matrix.timesAssign(inverse)
+ }
+ }
+
companion object {
private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix ->
val newMatrix = view.matrix
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index fe922bc..9b725ee 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -167,6 +167,10 @@
override val viewRoot: View get() = this
+ init {
+ clipChildren = false
+ }
+
var factory: ((Context) -> T)? = null
set(value) {
field = value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 6f7cfe9..2a8772f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.node.LayoutNodeWrapper
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -93,6 +94,12 @@
fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect
/**
+ * Modifies [matrix] to be a transform to convert a coordinate in [sourceCoordinates]
+ * to a coordinate in `this` [LayoutCoordinates].
+ */
+ fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {}
+
+ /**
* Returns the position in pixels of an [alignment line][AlignmentLine],
* or [AlignmentLine.Unspecified] if the line is not provided.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
index 571029f..cb2fd62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -77,6 +77,11 @@
* Returns true if this layout is currently a part of the layout tree.
*/
val isAttached: Boolean
+
+ /**
+ * Unique and stable id representing this node to the semantics system.
+ */
+ val semanticsId: Int
}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index 828213c..dc47e9e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -21,6 +21,7 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.node.LayoutNodeWrapper
import androidx.compose.ui.node.LookaheadDelegate
import androidx.compose.ui.unit.IntSize
@@ -107,6 +108,10 @@
clipBounds: Boolean
): Rect = wrapper.localBoundingBoxOf(sourceCoordinates, clipBounds)
+ override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+ wrapper.transformFrom(sourceCoordinates, matrix)
+ }
+
override fun get(alignmentLine: AlignmentLine): Int = wrapper.get(alignmentLine)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c973b88..a5b4070 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.SemanticsEntity
+import androidx.compose.ui.semantics.SemanticsModifierCore.Companion.generateSemanticsId
import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -74,7 +75,9 @@
// virtual nodes will be treated as the direct children of the virtual node parent.
// This whole concept will be replaced with a proper subcomposition logic which allows to
// subcompose multiple times into the same LayoutNode and define offsets.
- private val isVirtual: Boolean = false
+ private val isVirtual: Boolean = false,
+ // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
+ override val semanticsId: Int = generateSemanticsId()
) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
Owner.OnLayoutCompletedListener {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 6191cb2..34fef50 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.DefaultCameraDistance
import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
@@ -717,6 +718,43 @@
return ancestorToLocal(commonAncestor, position)
}
+ override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+ val layoutNodeWrapper = sourceCoordinates.toWrapper()
+ val commonAncestor = findCommonAncestor(layoutNodeWrapper)
+
+ matrix.reset()
+ // Transform from the source to the common ancestor
+ layoutNodeWrapper.transformToAncestor(commonAncestor, matrix)
+ // Transform from the common ancestor to this
+ transformFromAncestor(commonAncestor, matrix)
+ }
+
+ private fun transformToAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+ var wrapper = this
+ while (wrapper != ancestor) {
+ wrapper.layer?.transform(matrix)
+ val position = wrapper.position
+ if (position != IntOffset.Zero) {
+ tmpMatrix.reset()
+ tmpMatrix.translate(position.x.toFloat(), position.y.toFloat())
+ matrix.timesAssign(tmpMatrix)
+ }
+ wrapper = wrapper.wrappedBy!!
+ }
+ }
+
+ private fun transformFromAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+ if (ancestor != this) {
+ wrappedBy!!.transformFromAncestor(ancestor, matrix)
+ if (position != IntOffset.Zero) {
+ tmpMatrix.reset()
+ tmpMatrix.translate(-position.x.toFloat(), -position.y.toFloat())
+ matrix.timesAssign(tmpMatrix)
+ }
+ layer?.inverseTransform(matrix)
+ }
+ }
+
override fun localBoundingBoxOf(
sourceCoordinates: LayoutCoordinates,
clipBounds: Boolean
@@ -1139,6 +1177,10 @@
private val graphicsLayerScope = ReusableGraphicsLayerScope()
private val tmpLayerPositionalProperties = LayerPositionalProperties()
+ // Used for matrix calculations. It should not be used for anything that could lead to
+ // reentrancy.
+ private val tmpMatrix = Matrix()
+
/**
* Hit testing specifics for pointer input.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index 73d3ce0..a12e478 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -121,4 +121,16 @@
* as new after this call.
*/
fun reuseLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit)
+
+ /**
+ * Calculates the transform from the parent to the local coordinates and multiplies
+ * [matrix] by the transform.
+ */
+ fun transform(matrix: Matrix)
+
+ /**
+ * Calculates the transform from the layer to the parent and multiplies [matrix] by
+ * the transform.
+ */
+ fun inverseTransform(matrix: Matrix)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
index 164d52e..54e3dfa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
@@ -26,6 +26,9 @@
wrapped: LayoutNodeWrapper,
modifier: SemanticsModifier
) : LayoutNodeEntity<SemanticsEntity, SemanticsModifier>(wrapped, modifier) {
+ val id: Int
+ get() = layoutNode.semanticsId
+
private val useMinimumTouchTarget: Boolean
get() = modifier.semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
@@ -56,7 +59,7 @@
}
override fun toString(): String {
- return "${super.toString()} id: ${modifier.id} config: ${modifier.semanticsConfiguration}"
+ return "${super.toString()} semanticsId: $id config: ${modifier.semanticsConfiguration}"
}
fun touchBoundsInRoot(): Rect {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 142f6fd..de40d11 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -16,10 +16,11 @@
package androidx.compose.ui.semantics
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.platform.AtomicInt
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.NoInspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -29,12 +30,12 @@
*/
@JvmDefaultWithCompatibility
interface SemanticsModifier : Modifier.Element {
- /**
- * The unique id of this semantics.
- *
- * Should be generated from SemanticsModifierCore.generateSemanticsId().
- */
- val id: Int
+ @Deprecated(
+ message = "SemanticsModifier.id is now unused and has been set to a fixed value. " +
+ "Retrieve the id from LayoutInfo instead.",
+ replaceWith = ReplaceWith("")
+ )
+ val id: Int get() = -1
/**
* The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
@@ -44,18 +45,18 @@
}
internal class SemanticsModifierCore(
- override val id: Int,
mergeDescendants: Boolean,
clearAndSetSemantics: Boolean,
- properties: (SemanticsPropertyReceiver.() -> Unit)
-) : SemanticsModifier {
+ properties: (SemanticsPropertyReceiver.() -> Unit),
+ inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
+) : SemanticsModifier, InspectorValueInfo(inspectorInfo) {
override val semanticsConfiguration: SemanticsConfiguration =
SemanticsConfiguration().also {
it.isMergingSemanticsOfDescendants = mergeDescendants
it.isClearingSemantics = clearAndSetSemantics
-
it.properties()
}
+
companion object {
private var lastIdentifier = AtomicInt(0)
fun generateSemanticsId() = lastIdentifier.addAndGet(1)
@@ -64,15 +65,12 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SemanticsModifierCore) return false
-
- if (id != other.id) return false
if (semanticsConfiguration != other.semanticsConfiguration) return false
-
return true
}
override fun hashCode(): Int {
- return 31 * semanticsConfiguration.hashCode() + id.hashCode()
+ return semanticsConfiguration.hashCode()
}
}
@@ -109,16 +107,16 @@
fun Modifier.semantics(
mergeDescendants: Boolean = false,
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = mergeDescendants,
+ clearAndSetSemantics = false,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "semantics"
this.properties["mergeDescendants"] = mergeDescendants
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants, clearAndSetSemantics = false, properties)
-}
+)
/**
* Clears the semantics of all the descendant nodes and sets new semantics.
@@ -137,12 +135,12 @@
*/
fun Modifier.clearAndSetSemantics(
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = false,
+ clearAndSetSemantics = true,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "clearAndSetSemantics"
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants = false, clearAndSetSemantics = true, properties)
-}
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 1701b58..1a45eb5d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -64,7 +64,6 @@
private var fakeNodeParent: SemanticsNode? = null
internal val unmergedConfig = outerSemanticsEntity.collapsedSemanticsConfiguration()
- val id: Int = outerSemanticsEntity.modifier.id
/**
* The [LayoutInfo] that this is associated with.
@@ -81,6 +80,8 @@
*/
internal val layoutNode: LayoutNode = outerSemanticsEntity.layoutNode
+ val id: Int = layoutNode.semanticsId
+
// GEOMETRY
/**
@@ -379,9 +380,12 @@
): SemanticsNode {
val fakeNode = SemanticsNode(
outerSemanticsEntity = SemanticsEntity(
- wrapped = LayoutNode(isVirtual = true).innerLayoutNodeWrapper,
+ wrapped = LayoutNode(
+ isVirtual = true,
+ semanticsId =
+ if (role != null) roleFakeNodeId() else contentDescriptionFakeNodeId()
+ ).innerLayoutNodeWrapper,
modifier = SemanticsModifierCore(
- if (role != null) this.roleFakeNodeId() else contentDescriptionFakeNodeId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = properties
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index c672f75..b420d49 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -115,7 +115,6 @@
override val sharedDrawScope = LayoutNodeDrawScope()
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
index b219330..49c7b5d 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
@@ -232,6 +232,14 @@
canvas.restore()
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(getMatrix(inverse = false))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ matrix.timesAssign(getMatrix(inverse = true))
+ }
+
private fun performDrawLayer(canvas: Canvas, bounds: Rect) {
if (alpha > 0) {
if (shadowElevation > 0) {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index b561f1d..cd84292 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
@@ -46,6 +47,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.RootMeasurePolicy.measure
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.AccessibilityManager
@@ -53,6 +55,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsEntity
import androidx.compose.ui.semantics.SemanticsModifier
@@ -824,6 +827,204 @@
}
@Test
+ fun layoutNodeWrapper_transformFrom_offsets() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ parent.place(-100, 10)
+ child.place(50, 80)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(-50f, -80f), matrix.map(Offset.Zero))
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(50f, 80f), matrix.map(Offset.Zero))
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_translation() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ translationX = 5f
+ translationY = 2f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(-5f, matrix.map(Offset.Zero).x, 0.001f)
+ assertEquals(-2f, matrix.map(Offset.Zero).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(5f, matrix.map(Offset.Zero).x, 0.001f)
+ assertEquals(2f, matrix.map(Offset.Zero).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_rotation() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ rotationZ = 90f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+ assertEquals(-1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_scale() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ scaleX = 0f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ // The X coordinate is somewhat nonsensical since it is scaled to 0
+ // We've chosen to make it not transform when there's a nonsensical inverse.
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ // This direction works, so we can expect the normal scaling
+ assertEquals(0f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ child.innerLayoutNodeWrapper.onLayerBlockUpdated {
+ scaleX = 0.5f
+ scaleY = 0.25f
+ }
+
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(2f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(4f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0.5f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(0.25f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_siblings() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child1 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child1)
+ child1.modifier = Modifier.graphicsLayer {
+ scaleX = 0.5f
+ scaleY = 0.25f
+ transformOrigin = TransformOrigin(0f, 0f)
+ }
+ val child2 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child2)
+ child2.modifier = Modifier.graphicsLayer {
+ scaleX = 5f
+ scaleY = 2f
+ transformOrigin = TransformOrigin(0f, 0f)
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child1.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child1.outerLayoutNodeWrapper), Constraints())
+ child2.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child2.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child1.place(100, 200)
+ child2.place(5, 11)
+
+ val matrix = Matrix()
+ child2.innerLayoutNodeWrapper.transformFrom(child1.innerLayoutNodeWrapper, matrix)
+
+ // (20, 36) should be (10, 9) in real coordinates due to scaling
+ // Translate to (110, 209) in the parent
+ // Translate to (105, 198) in child2's coordinates, discounting scale
+ // Scaled to (21, 99)
+ val offset = matrix.map(Offset(20f, 36f))
+ assertEquals(21f, offset.x, 0.001f)
+ assertEquals(99f, offset.y, 0.001f)
+
+ child1.innerLayoutNodeWrapper.transformFrom(child2.innerLayoutNodeWrapper, matrix)
+ val offset2 = matrix.map(Offset(21f, 99f))
+ assertEquals(20f, offset2.x, 0.001f)
+ assertEquals(36f, offset2.y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_cousins() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child1 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child1)
+ val child2 = ZeroSizedLayoutNode()
+ parent.insertAt(1, child2)
+
+ val grandChild1 = ZeroSizedLayoutNode()
+ child1.insertAt(0, grandChild1)
+ val grandChild2 = ZeroSizedLayoutNode()
+ child2.insertAt(0, grandChild2)
+
+ parent.place(-100, 10)
+ child1.place(10, 11)
+ child2.place(22, 33)
+ grandChild1.place(45, 27)
+ grandChild2.place(17, 59)
+
+ val matrix = Matrix()
+ grandChild1.innerLayoutNodeWrapper.transformFrom(grandChild2.innerLayoutNodeWrapper, matrix)
+
+ // (17, 59) + (22, 33) - (10, 11) - (45, 27) = (-16, 54)
+ assertEquals(Offset(-16f, 54f), matrix.map(Offset.Zero))
+
+ grandChild2.innerLayoutNodeWrapper.transformFrom(grandChild1.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(16f, -54f), matrix.map(Offset.Zero))
+ }
+
+ @Test
fun hitTest_pointerInBounds_pointerInputFilterHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
@@ -1134,7 +1335,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode =
@@ -1157,7 +1357,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
@@ -1176,11 +1375,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -1238,11 +1435,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -2384,6 +2579,8 @@
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
): OwnedLayer {
+ val transform = Matrix()
+ val inverseTransform = Matrix()
return object : OwnedLayer {
override fun updateLayerProperties(
scaleX: Float,
@@ -2405,6 +2602,12 @@
layoutDirection: LayoutDirection,
density: Density
) {
+ transform.reset()
+ // This is not expected to be 100% accurate
+ transform.scale(scaleX, scaleY)
+ transform.rotateZ(rotationZ)
+ transform.translate(translationX, translationY)
+ transform.invertTo(inverseTransform)
}
override fun isInLayer(position: Offset) = true
@@ -2437,6 +2640,14 @@
) {
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(transform)
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ matrix.timesAssign(inverseTransform)
+ }
+
override fun mapOffset(point: Offset, inverse: Boolean) = point
}
}
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
index e3d6b20..707ae1b 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
@@ -54,12 +54,18 @@
}
@Test
- fun testCreate_success() {
+ fun testCreate_success_enableMlock() {
val counter: SharedCounter = SharedCounter.create { testFile }
assertThat(counter).isNotNull()
}
@Test
+ fun testCreate_success_disableMlock() {
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
+ assertThat(counter).isNotNull()
+ }
+
+ @Test
fun testCreate_failure() {
val tempFile = tempFolder.newFile()
tempFile.setReadable(false)
@@ -72,13 +78,13 @@
@Test
fun testGetValue() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
assertThat(counter.getValue()).isEqualTo(0)
}
@Test
fun testIncrementAndGet() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
for (count in 1..100) {
assertThat(counter.incrementAndGetValue()).isEqualTo(count)
}
@@ -86,7 +92,7 @@
@Test
fun testIncrementInParallel() = runTest {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
val valueToAdd = 100
val numCoroutines = 10
val numbers: MutableSet<Int> = mutableSetOf()
@@ -105,4 +111,20 @@
assertThat(numbers).contains(num)
}
}
+
+ @Test
+ fun testManyInstancesWithMlockDisabled() = runTest {
+ // More than 16
+ val numCoroutines = 5000
+ val counters = mutableListOf<SharedCounter>()
+ val deferred = async {
+ repeat(numCoroutines) {
+ val tempFile = tempFolder.newFile()
+ val counter = SharedCounter.create(false) { tempFile }
+ assertThat(counter.getValue()).isEqualTo(0)
+ counters.add(counter)
+ }
+ }
+ deferred.await()
+ }
}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
index 892157e..61cddca 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
@@ -35,10 +35,19 @@
extern "C" {
JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeTruncateFile(
JNIEnv *env, jclass clazz, jint fd) {
+ if (int errNum = datastore::TruncateFile(fd)) {
+ return ThrowIoException(env, strerror(errNum));
+ }
+ return 0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+ JNIEnv *env, jclass clazz, jint fd, jboolean enable_mlock) {
void* address = nullptr;
- if (int errNum = datastore::CreateSharedCounter(fd, &address)) {
+ if (int errNum = datastore::CreateSharedCounter(fd, &address, enable_mlock)) {
return ThrowIoException(env, strerror(errNum));
}
return reinterpret_cast<jlong>(address);
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
index 9dcf9da..68f5ce1 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
@@ -39,16 +39,24 @@
namespace datastore {
+int TruncateFile(int fd) {
+ return (ftruncate(fd, NUM_BYTES) == 0) ? 0 : errno;
+}
+
/*
- * This function returns non-zero errno if fails to create the counter. Caller should use
- * "strerror(errno)" to get error message.
+ * This function returns non-zero errno if fails to create the counter. Caller should have called
+ * "TruncateFile" before calling this method. Caller should use "strerror(errno)" to get error
+ * message.
*/
-int CreateSharedCounter(int fd, void** counter_address) {
- if (ftruncate(fd, NUM_BYTES) != 0) {
- return errno;
- }
- void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_LOCKED, fd, 0);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock) {
+ // Map with MAP_SHARED so the memory region is shared with other processes.
+ // MAP_LOCKED may cause memory starvation (b/233902124) so is configurable.
+ int map_flags = MAP_SHARED;
+ // TODO(b/233902124): the impact of MAP_POPULATE is still unclear, experiment
+ // with it when possible.
+ map_flags |= enable_mlock ? MAP_LOCKED : MAP_POPULATE;
+
+ void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE, map_flags, fd, 0);
if (mmap_result == MAP_FAILED) {
return errno;
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
index 756e2fe..cf73095 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
@@ -21,7 +21,8 @@
#define DATASTORE_SHARED_COUNTER_H
namespace datastore {
-int CreateSharedCounter(int fd, void** counter_address);
+int TruncateFile(int fd);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock);
uint32_t GetCounterValue(std::atomic<uint32_t>* counter);
uint32_t IncrementAndGetCounterValue(std::atomic<uint32_t>* counter);
} // namespace datastore
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index eb64a3c..e01662d 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -25,7 +25,8 @@
* Put the JNI methods in a separate class to make them internal to the package.
*/
internal class NativeSharedCounter {
- external fun nativeCreateSharedCounter(fd: Int): Long
+ external fun nativeTruncateFile(fd: Int): Int
+ external fun nativeCreateSharedCounter(fd: Int, enableMlock: Boolean): Long
external fun nativeGetCounterValue(address: Long): Int
external fun nativeIncrementAndGetCounterValue(address: Long): Int
}
@@ -57,22 +58,28 @@
fun loadLib() = System.loadLibrary("datastore_shared_counter")
@SuppressLint("SyntheticAccessor")
- private fun createCounterFromFd(pfd: ParcelFileDescriptor): SharedCounter {
+ private fun createCounterFromFd(
+ pfd: ParcelFileDescriptor,
+ enableMlock: Boolean
+ ): SharedCounter {
val nativeFd = pfd.getFd()
- val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd)
+ if (nativeSharedCounter.nativeTruncateFile(nativeFd) != 0) {
+ throw IOException("Failed to truncate counter file")
+ }
+ val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd, enableMlock)
if (address < 0) {
- throw IOException("Failed to mmap or truncate counter file")
+ throw IOException("Failed to mmap counter file")
}
return SharedCounter(address)
}
- internal fun create(produceFile: () -> File): SharedCounter {
+ internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
val file = produceFile()
return ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
).use {
- createCounterFromFd(it)
+ createCounterFromFd(it, enableMlock)
}
}
}
diff --git a/development/JetpadClient.py b/development/JetpadClient.py
index 1e15f07..19b74bc 100644
--- a/development/JetpadClient.py
+++ b/development/JetpadClient.py
@@ -33,7 +33,7 @@
return None
rawJetpadReleaseOutputLines = rawJetpadReleaseOutput.splitlines()
if len(rawJetpadReleaseOutputLines) <= 2:
- print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % args.date)
+ print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % date)
return None
jetpadReleaseOutput = iter(rawJetpadReleaseOutputLines)
return jetpadReleaseOutput
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
index 013c163..8ce954a 100755
--- a/development/auto-version-updater/update_versions_for_release.py
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -19,10 +19,6 @@
import argparse
from datetime import date
import subprocess
-from shutil import rmtree
-from shutil import copyfile
-from distutils.dir_util import copy_tree
-from distutils.dir_util import DistutilsFileError
import toml
# Import the JetpadClient from the parent directory
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 202d561..f12d43e 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -71,7 +71,6 @@
"androidx/heifwriter"
"androidx/hilt"
"androidx/leanback"
- "androidx/loader"
"androidx/media"
"androidx/media2"
"androidx/mediarouter"
@@ -123,7 +122,6 @@
"androidx/heifwriter"
"androidx/hilt"
"androidx/leanback"
- "androidx/loader"
"androidx/media"
"androidx/media2"
"androidx/mediarouter"
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index a2b8326..b4d24d9 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -140,9 +140,9 @@
docs("androidx.enterprise:enterprise-feedback:1.1.0")
docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
docs("androidx.exifinterface:exifinterface:1.3.3")
- docs("androidx.fragment:fragment:1.5.0")
- docs("androidx.fragment:fragment-ktx:1.5.0")
- docs("androidx.fragment:fragment-testing:1.5.0")
+ docs("androidx.fragment:fragment:1.5.1")
+ docs("androidx.fragment:fragment-ktx:1.5.1")
+ docs("androidx.fragment:fragment-testing:1.5.1")
docs("androidx.glance:glance:1.0.0-alpha03")
docs("androidx.glance:glance-appwidget:1.0.0-alpha03")
docs("androidx.glance:glance-appwidget-proto:1.0.0-alpha03")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 348abec..ffc1efc 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -169,6 +169,7 @@
docs(project(":hilt:hilt-navigation-fragment"))
docs(project(":hilt:hilt-work"))
docs(project(":interpolator:interpolator"))
+ docs(project(":javascriptengine:javascriptengine"))
docs(project(":metrics:metrics-performance"))
docs(project(":leanback:leanback"))
docs(project(":leanback:leanback-paging"))
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 982978f..4c8fc2f 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(project(":fragment:fragment"))
- api("androidx.activity:activity-ktx:1.5.0") {
+ api("androidx.activity:activity-ktx:1.5.1") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
api("androidx.core:core-ktx:1.2.0") {
@@ -34,10 +34,10 @@
api("androidx.collection:collection-ktx:1.1.0") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.0") {
+ api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.1") {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0") {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 309118a..02e8a8a 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,10 +29,10 @@
api("androidx.collection:collection:1.1.0")
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
- api("androidx.activity:activity:1.5.0")
- api("androidx.lifecycle:lifecycle-livedata-core:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.activity:activity:1.5.1")
+ api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
api("androidx.savedstate:savedstate:1.2.0")
api("androidx.annotation:annotation-experimental:1.0.0")
api(libs.kotlinStdlib)
diff --git a/gradle.properties b/gradle.properties
index 18c3692..c17e4a1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,7 +24,7 @@
android.experimental.lint.missingBaselineIsEmptyBaseline=true
# Don't generate versioned API files
-androidx.writeVersionedApiFiles=false
+androidx.writeVersionedApiFiles=true
# Don't warn about needing to update AGP
android.suppressUnsupportedCompileSdk=Tiramisu,33
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97b429e..dc99e9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,7 +150,7 @@
multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
-okio = { module = "com.squareup.okio:okio", version = "3.0.0" }
+okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
playCore = { module = "com.google.android.play:core", version = "1.10.3" }
playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 5d3ed73..05c098b 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -323,6 +323,43 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 07D3516820BCF6B1
+uid Ben Manes <ben.manes@gmail.com>
+
+sub 11F4CE313A637CC1
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBF3HgdMBCAC3ET5ipFXdZ9GGMbtsCQ3HGT40saajsNDOdov2nMJxzKkVe3wk
+sN3bpgbsqBU9ykVkIhX8zV5+v8DOBzkV0pJ2eLjFa9jBPvNjV+KoK2BAI5pzNzYg
+sHPwo1aRXdI0MvCy+7iaIiiGF4/O16AhU4LmALHnaRQZCyuN6VOQ8rlqNvcczwUf
+J2DQeLHqR/tsch7S01hGpPAptBeu19PyAlQsntYN0yLCLKoe9dFXWCDkvd1So5LF
+6So+ryPqupumBbh4WxCmTp9qwDJYJItjAE0zyPe890FurOtxrFTwtRtX6d6qGKkY
+/B4T3r0tTE1EiOUpmSnxmGNItMh7/l5UtnHjABEBAAG0H0JlbiBNYW5lcyA8YmVu
+Lm1hbmVzQGdtYWlsLmNvbT6JAU4EEwEIADgWIQRjXuYnNF88HdQisuIH01FoILz2
+sQUCXceB0wIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAH01FoILz2sdoo
+B/0YUh73jUMl14MjWvp9zrFHN8h+LqB4NMQcP93RdPTtDKi0a+0h8gQtm0D+K49Q
+BQbFztOObfZS3kdJ3VOqmodScWrGtMU3HsYT2ioQalqbYvl9FIPDrlOjHaZgwgyJ
+We0DVKHRApbtIh+NxTpQUJtanxgF60ZtOoToZe8XMGc9LaCZcrFxK/AlMdDMgUCx
+qzBbXhAcvut2bJVL5B4kLNMABrbUuFMjTNI4JxvgTXKL/jNk6XPtCjdmgIh7mT/G
+Mpu9t3i1zegAPdM5N/MAgiGHqm+blANLniSAbZja8Ny7211fwOYoJ546VPwDjL7B
+rBlymB3COoYZhql2DcBBg39cuQENBF3HgdMBCACu3VQKKmagcPbcMZOqbDXE5iK3
+0G742rCpf/j3ywnwTZJQ/58HtAi8+/fXxUhTHswoON2TwiiHrHAkObe+K9A+jv0E
+xjKVMmQ/sOCYWZDEGMth4yJnzDbT1Tlm/l2i5Lv0ZaD7fTEhtprQNuU06dveTeJs
+zDyqtK9T80mvI4+GH59wM80l1y6uj8KA4pY0PdSFgbyS9iAFADGsUsc6t1KiZ5W1
+9odMjDPlQtJ20pm5CvJlDZbYNRJ54CSldZikRvmNRg5mWdRLNfbRMFDLFfcdYLdO
+WJXnAt9cKFJC9P//ItZFrlhu3akTH//HF2kxQNW61Sd92/xtFUD/2tN1GlXfABEB
+AAGJATYEGAEIACAWIQRjXuYnNF88HdQisuIH01FoILz2sQUCXceB0wIbDAAKCRAH
+01FoILz2saySCACibIpnls5wJkfX1B/7tDjWk2hEGZYcASr0xp/DDwSgJ5edByuQ
+NQF7RHuCk0ke6IQGfytMLJlXeEIu79DvgPakxBP5iG+c095FbhRu+9nCEkRqQvop
+4fA7ZdhuerOyuObWz8+o3Z2RywWPXlK+F/9iJiO/qtvmdORuikJtN9VxgvAUvANZ
+RtlzjL296p0TJzGqXhyer46CHl/Yj7TtX6EpnZDgiaQbOWRFOZ5x81xI79bQD7Ew
+DzfrwQHbjQDkqhkwOoV6Wq239ZaHh6p7GXHnQkDMQ0H/7Y2tw6PH5VM8fDJkJKF2
+PIukJrUXa06KqrdZ9YxqvSmu5UY6tMSRwGWp
+=/wFN
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 083891AD4774845A
uid Eclipse Project for JAXB <jaxb-dev@eclipse.org>
@@ -3715,6 +3752,46 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 5B05CCDE140C2876
+sub 9D29AE4A6B50E01F
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q
+YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs
+en3ClYLXT5UVx0H1ZfDVKCvmaZVirZInfkqbi3OiPQoWrUfu02c3DiHQJ+Y34kdB
+egH2sIShNH8WLfEZ3YDQ4XaWHVuN1C7VwCBM8R3OeTTfyDrTsuyqJ0SeZXRR/6df
+2pEckjF9DNSXyjzFg24MrZhuhgbnj0oR1zmRh1EF+KlBfF4DF4acfxWqqcJVJx/3
+FTtOkLe3Xjj+inyJgxOW52gD4DsJpyf1tIbjAQDZvOdlRRCqZB4FnzzIb/1GmkGD
+JpDLC4MQmqgxkm0n8wgAmmHLpqDTdmuyJXvdX9RdGycpW64sljd1mpzTWJ8UKDhj
+uFQVHSSEdLoHoVj8ItnBV2kXd2uoQ/tWzbxFBST7wE/tX0e9G5XWaPKogvOKeDus
+u9XTIds2krYp80UTYWFZ88oNwGikdIrEYURSYDsYt15miROtKHWbSOHeLVbZqgVx
+dtWPqQVfH4gJGEH97/OSmozqDVog1aZDKBLGZQosng5h4j2RAQpjkaIdxKl8m7CQ
+x0Yi9tA8yD1QhRGggANQIb4n00G/vm7RMU/7NBvvjAQ/nAFjbsyO5oX1rBY1M6Xo
+NFqIBrHSBzV9MmhS3nXU+ZjAktCRhyJ7TsoHM0OYEAf8CduM5Zzp5w02iVYkFQBB
+wAoKHMpycW5LhMMMS4w7gmOfP7y04rtg6+zVe41y6bOqn/SxHCcCgnE/nhdexlzH
+ElYE1H7+HpphoI5vEwS6uElF67CoO5r74Zrb6nshGEj2AoOqjbrsdQm0noBBNYAu
+f9RsjU0sQQFzLW8+2xahqK3oZkLWOkSxzLtVwJbm7EGaGIYxEBjg87OnGQkAi9vv
+tVPwdO3VWyvgKLuPHudLDhTpeH3AMbzKgnru1Pnh/ZpiRhPzsbuFtFPEX8PMuCyE
+n4OLzUALl98kXuPjG5ww+24UsNgKMbKbu8qq/zRu7IHlpZvd730RoCWU2/i18tnY
+zLkCDQRMFWTlEAgA+MQFGIhyA4Ww9g7J8ZiEltwSzRblrjM1q9anexsBIGsWH37A
+92rlVK1RzMVfhj5yl+BzIBGO+zHbgycX7iB5/Fwsm+6R/2Uich6NDm1Qai9rc/jg
+3MS0phOAQzgxlGKOTS2GzdbDJCBQMijDObNe+Cs5DNB/E29/nzzCTQvtRzSeplZN
+r+8Q8lWz6efXmm5EeeZxN4x1YXjjzMJCHbc3yGxOjTgYQOs962yUYsg9UDRJm1OH
+9NKZe1m3dTRIMUcZvL12dq/kyiHHR9V/6CkdiNw1AFMi3tvEdvX4D1k1/Qr/2ORZ
+E4lRzgug4sKkpgaclLnkJZ9EMczmUFTGbbkx3wADBQf/Y+2nZCJSuHiDv/+SdhQh
+OBapZ2hYPDvg29mpPqin/LwH7eFTNv/oos1wzuzGtTHHGEP5mUQLOxjwdAXsWMMj
+scSbCs66ytTN7X4O8qh+1yN7vrM6ZBL12Ix7Ku40cgkWyvTVLBXKaEGm4ElhAmSL
+Fpu+/fJw0riR6rIuwHcGB4R1IJtMWcj+b1odgw9QmJ8AGpHh2WVdXspoCGnTUN4m
+DEswZjplkKXCgLypU13SrHVOqhjd4caK5GNZUfWtCKtwNcJMnvgp2truMvh9BBn6
+widfK48hEknQtXzGjui+bZz2/AD7/OT/T1CqDspB8IQlBCMBn8J4U1grSrZ1wTJf
+HIhnBBgRCAAPBQJMFWTlAhsMBQkLRzUAAAoJEFsFzN4UDCh23wsBANDSDn2KWz7H
+b5geDwUTX4T8Uqn21eFbp54tFTfopCd/AP4nTdX1iahsClr9q6G+CWQBuQWHVmq3
+FlPU/jTn6vXQwA==
+=dKtU
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 5D67BFFCBA1F9A39
sub DBE749136BF76809
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -3833,6 +3910,39 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub DEE12B9896F97E34
+sub 9A716F957BC42546
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFAxQKwBCADJGPv6pmFEq0SDwAKESEgCdnXycbR0bNXpNa/3VGboNto1xKgd
+AQ/sI5x+CmN0hpUjklEwff6QIt3MlofEMkAzSfRmTobhJTK9W7r4+p5DuhJpi5Wz
+ITdbNCMT3Cvp13rRE+dx9qY+WFQmTYPf3gq+C6T8Q1i35ePNlCTN2RayaFxxR77D
+W93zKZDdd7I1qH0Vx7GGcSwBgBlEB8jmhNAkz/zAhv53S6px3ZttqYYmuwRtg6Fi
+i/u9VoDR/c9tyUq8L6oAUtg0mo4CP/tfUF/uZnibshEsLzbRP961VQXduhn8HcRp
+k6QPTj37B1vsNWJ9U7XXJ6pYnkizQo7sl5XxABEBAAG5AQ0EUDFArAEIALyNR+z1
+eBBF4S+dOEWKXz2ANmsp6RRhvR09QeQwNycVdbdEXpOiSZUCAkw/EhuJWmHBngat
+0KBO+7CIHyQqwHnqyatizzKXi1OuaEhMzPsQMwPRfYyWHgN0aklc5oOzB2RbSJN4
+et/oVvfAplVSjgR0v+56+qXw9TFlp4kxqFeJLycZ+5ImKQ+XclsBokKuE7cjeF+g
+O5oY/CFHdkxD8d+cLF8FSNUFMypuDQ4IH9zPYGkUJqsb2t67iMyxi14RqyN2YNqK
+JcwxTL42VBlUFlTBoF2Y3w0LNll6pR2WSNvpcj+5/uBjtY1qAj5e7yVts+d1YZsX
+7D76AV742RQ31kkAEQEAAYkCRAQYAQIADwUCUDFArAIbLgUJB4YfgAEpCRDe4SuY
+lvl+NMBdIAQZAQIABgUCUDFArAAKCRCacW+Ve8QlRhFDB/9xE/cXf5fVaLa598xL
+muXiD9U1B04dPdz445/chdDS9iGWBB+5QVvAqv2Jt0hyPN0+n9Mk/4lLStEEL8TP
+NLdTBP1JRvVWC1c+G3kTJq05Abj8CGFFm1UZhFRwCTJ+vrv8fSb15s+YYxBLIUdl
+tKld6OupTHm8A4XJQOhYxd5PHs72bJ3bXs4GmPLKD/RpYmXYJ9EZHQHKnrhZKJ8R
+JKTM6sxBrgdVeI1K0ekA0o5HAVpNEXgY1gG8Pa14jqK0iwlcI02ntqeJkobvv1wN
+vh+nJT2wM5QyLH737kdPrUdi63PfCYLOEHYhI6sFkzI/DAtI/C3wmHtTuRam3aLs
+Rnb7GNQH/i07ndoI4trmUor3X1JBbcjw2BVS+idCtML3jhKtziwK2/kz0rJqBQKa
+Z/zxgEfwkRPqhXLaBW8a1G/d1mGphazHqSaDqylz07XqR31ZtGCc6256anaVbWaW
+9HXUsU5ADNrAK9PdD0EibGB8YumuSTtApICUqN5SVz+h3Mi1MXVsmbiVSAZPzLTD
+0YRwzPJ3jiXIrKDUmZMM7oWwGx6nzW++tW8aKyLKm7x1/y8g+XHvySQiVOKAvvxj
+yPStkEW38Rls5nucpyLzLjoA5vlyIcOkeKCy2jlUmM56YrAIWNn/eCRFPHMOY1DO
+B1nUXMr+2W21xZO+/sWrEEysY0mdGU0=
+=uzFx
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 5F69AD087600B22C
uid Eric Bruneton <ebruneton@free.fr>
@@ -9496,6 +9606,49 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub D364ABAA39A47320
+sub 3F606403DCA455C8
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr
+c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD
+m+1zSFswH2bOqeLSbFZPQ9sVIOzO6AInaOTOoecHChHnUztAhRIOIUYmhABJGiu5
+jCP5SStoXm8YtRWT1unJcduHQ51EztQe02k+RTratQ31OSkeJORle7k7cudCS+yp
+z5gTaS1Bx02v0Y8Qaw17vY9Pn8DmsECRvXL6K7ItX6zKkSdJYVGMtiF/kp4rg94I
+XodrlzrMGPGPga9fTcqMPvx/3ffwgIsgtgaKg7te++L3db/xx48XgZ2qYAU8GssE
+N14xRFQmr8sg+QiCIHL0Az88v9mILYOqgxa3RvQ79tTqAKwPg0o2w/wF/WU0Rw53
+mdNy9JTUjetWKuoTmDaXVZO4LQ2g4W2dQTbgHyomiIgV7BnLFUiqOLPo+imruSCs
+W31Arjpb8q6XGTwjySa8waJxHhyV2AvEdAHUIdNuhD4dmPKXszlfFZwXbo1OOuIF
+tUZ9lsOQiCpuO7IpIprLc8L9d1TRnCrfM8kxMbX4KVGajWL+c8FlLnUwR4gSxT1G
+qIgZZ09wL5QiTeGF3biS5mxvn+gF9ns2Ahr2QmMqA2k5AMBTJimmY/OSWwARAQAB
+uQINBGH0NlsBEAC9o6m+D2LubGjOJxLQB1BnfBOkFHadsbkb82QFdrCNsd44fJie
+aqZVP+6XHKVRHSPktwpE1FnjThBJJsLwwcvwWXwDwvED57n4bATPlrPGuG7x+LRV
+bxFBTd+LQUCcHd3puruvbEjQdV54mbgdMqAp5dSA4Fc6h2hMWVBX4EdLiH/0ui3l
+UoqYTJcB73U1/jbKcbs0+cVuXIpmAPQpIs30p0wWLOKiJqn9tTZpwfntnrdfLvKL
+3FZcRQeWZjqH1Ywt4zWlCRqGEp7yVqhK5gn4nfEdSX2koxr53OOsGk2Pjhzs/5XJ
+Li1FTOcnja5kkqOPiPGB/BxAnjPCEsSiOFmF3Af4WdYa3+TK8+ggBSEeLjjLa5zy
+qexfhADwgb5ASZitUErJZDhAvqHGwfz3VPENy3K2kJLH+maWwOT1ZRoJnz3fxwIu
+gKhPx1MzlwhTclIknK7q2CNcB61pC9lg70ICW090NgknE2DtmjrRMONhcSkuWGLZ
+BKBgRqNwITJFcAdg6+ffZzGLsnEd+6A29PdsXfLS9KJqiabvpiBg8RaAAWiv5Tqs
+Nu9YSWUQUzBZO43u8AxTtThuHYZrxasoC3sCGIcRy2V9eaq480DRJ9uotONMutIH
+UDVSdqViPmmit0+PyRiCX/DOeBHumaEOm+RqIxPE8h6W8sHrYAQ7J1a3AQARAQAB
+iQI2BBgBCgAgFiEE7gyocwdAkvgG9Ztl02SrqjmkcyAFAmH0NlsCGwwACgkQ02Sr
+qjmkcyAsehAAps6j+qpjyNGUet/B6Z7nJcobSxnCIP/c+uUPD1oB6Uuht6NTYWQd
+wmEqL5BGz8WNTsBd0cQYvSztrMiz5tCDoiGGrWcgWxrrNxc1EVydhBbT4PpiG6CB
+WFCoEXN76/f0ndxZbjjobElTXbQ6oaLh2812OavgMdiJUVBgXrtfgi5/h49Wpc5o
+/IDM3bfujfrn5nvPIkd7Ee+GaK2YSCT7pfK4N/eW1g1SusqRQxBKCU3C5MVgVjkp
+Ba82U0kTxUGDFYUUcS+Yjhi/w4uynwIXW0pSl5wvxVVxNBfGFH5fkprkpcuVXp9B
+6SRVM85uUoZJFaIFyoAhU9uQQfVe6ugwP9BbhzRzDpJe9tiOcaazwzNnP5Zj31nI
+V6UltZu7mVSl1JwIcWxW3b36p4Ht9G5jIPQc8xS+oMd//p8r4sYFB4KOYas1ukRN
+iCshn9tJfeohkKj9ewxyUNf1rS8uOUJvZC3c3XRF8CJXRpxmHu2pPNf0QxFVhghL
+Y2cJU1OWGi6NyZN65EdfmkTbeDxdlSNv89STD4Vp6MmFtrA4JZDSR0Bp1zEPKiSx
+jpG5FpfVv6lXmFboa5qkXAHG9+bcaRYoXun+wJ3ioWo+cQEdy/bsX03+MHMsms8l
+ikmfPIGVw73RF3HXjJ8GVqTkqbo4ZpgTw/7Z3+fAYE/vxquhnpl2HvE=
+=5tlI
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub D57506CD188FD842
sub 63F72A7A8658D653
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -10915,3 +11068,50 @@
Pt2uco8an9pO9/oqU6vlZUr38w==
=alQS
-----END PGP PUBLIC KEY BLOCK-----
+
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: Hostname:
+Version: Hockeypuck 2.1.0-152-ga266fd3
+
+xsDNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
+xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
+N5N5gUj/dqVI2rIvypIuxUApl88BYMsxYpn2+8FKeMd8oBJLqFRJ3WNjB4Op2tRO
+XRWoxs1ypubS/IV1zkphHHpi6VSABlTyTWu4kXEj/1/GpsdtHRa9kvdWw7yKQbnM
+XuwOxtzZFJcyu0P2jYVfHHvxcjxuklc9edmCGdNxgKIoo0LXZOeFIi6OWtwzD0pn
+O6ovJ+PL9QscMdnQlPwsiCwjNUNue20GBv3aUIYc+Z8Gq0SqSan5V0IiKRHMJkzd
+FAhnpkSFBvHhPJn07BCcb1kctqL+xnLxIdi7arq3WNA/6bJjsojc/x3FdIvORIeP
+sqejhtL8mCBvbMAMHSBrFxclMp+HSz2ouHEEPIQam0KeN8t1yEqIy3/aYKMzHj9c
+C3s8XOaBCbJbKpMAEQEAAc09VG9iaWFzIFdhcm5la2UgKGZvciBkZXZlbG9wbWVu
+dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0PsLBFgQTAQgAQAIbDwcLCQgH
+AwIBBhUIAgkKCwQWAgMBAh4BAheAFiEE1HfVGBLmkgEdsR5mpuouK/IuBUMFAl+f
+HewFCREQdggACgkQpuouK/IuBUPAjgv+IvGD8arZP2epxB10nNxehgdB3vVGRvCz
+AWyw/d56KBwGN1czmlHINP/Ejfh4bRZgFXILISqcf+8rATvISsCgKzzfluOfDuFR
+puqZisrlaqEpDqUGK2R8x7kxARaB2G3g4dy6xyJZwv/5dfFPQJ/aQjeNkRSoXI4W
+WLNexZB3E0Gx9a3F32Xvr87vu9GchsoftxQft9joFupRg+kCipQ+w36D9gWmFXtj
+pYT3Wdrm0AcP6lezq+SpcwVn3+DW79p0/WOLhRr6NNQsRBIuM5nNIbCt8hnj9ule
+PZGctzwCTY8suID4Ru18NOiU8NKztoXII7XRloB9v5ezwktKoDzwTBgwm2+XM/vv
+GFlB09LaICdiuPQaiqSZbeLKKmBT1hTEtEHiPdMld2Hlji/rVYS3Ceiv0YUoOnmo
+AAEmtAG7ghpIJxyVtWZchZ55Hrb4oU5AntshrwYMWNRe0toxjQds5Ds2I2lqkjeU
+paUjQXEmPDS1hnckKAxI2PiOeifiLljxwsEWBBMBAgBAAhsPBwsJCAcDAgEGFQgC
+CQoLBBYCAwECHgECF4AWIQTUd9UYEuaSAR2xHmam6i4r8i4FQwUCW5n2GQUJDSpo
+eAAKCRCm6i4r8i4FQ9byDAC6yPry/EBRyJgpWXgLca8Dy56Oe9XtRA+kuAxq+c3q
+GmLy8JdBYxWeBI/dnjwzU6jCLLnY6eTigjSemHZRMPOoyxXYF47LpaoWL52JDi4R
+7xft+GD5Hy+tbDlYW5RVeMzR2Okg3XpvTmsYlcgSr6HCL0L7D25tpcFZMZrls9LN
+z80HetFk4LrR1LvVL8GpFv74xyWullpQU2QwnwXCzUpsXa9qOzwZltNIUfs4gVNG
+KhzfabYmMtlBAXzpi20bRWmJY4W+vGJKC9yWL1L4iu7LrIgMedqsKoMrl4Bg8xKE
+JGU0JEHWgfRopSr0FccP1bxWOaoJ2iN/v3Lifrk0T24vBA9cbTrnQmwrbNftJBLb
+7ccgkvkaFk+8qBe5t/OFgoV5zvmJ6xNEojpFnOtLfrPVpu8b7t3mcGVq1jQJ8afa
+8yIlQrLsA+ubA71pqgdv2ZhoWvL3R2wyxZGMX3xefqavJNxaziHGQorddrg9dyEO
+0xqXKDzjN5vuDTgSJimmZiHCwP8EEwECACkFAlJQhigCGw8FCQlmr/gHCwkIBwMC
+AQYVCAIJCgsEFgIDAQIeAQIXgAAKCRCm6i4r8i4FQ//CC/oD2LxmXHedlqlKl5WU
+EEFoXjDRpcSnfOTFdCn9U5bpBxM2gtlxNB4890TVga6C9kGfgkf9e11/ftdFQgHQ
+2LQKwpRaPOQdfk8Ek/oONmO6x6oIYXrVvY57xsW5AiFHUtPd84NJBoAyTePxstrJ
+TrFo0KQ8wX84rsU2XF/5CRCUuvx+Xomv1ALEed8Ajf9dhY85UTwIWXFINKwMTbNC
+neoBeUy3xugYEYWZCkrIk/iUvwA2pwqCwzHeDRomf1OTwW3VZ0U9/cfFyt3RgkU5
+goF55YOIpnKAjSkyygESaAs4kPrMtAJ6gy8lKsBEpxQfJWH6c5Q6MZn3RVb2S5Dx
+vlpCeiKIqnKtX1DnZrCZntt4Dwrrt4aFemLJ7+iaYndbMun3mAxG6Nqm+CfEOicG
+uTmFS6yakutYNOxJrxtz7yEIIt6yr5T3fQk6LhczhjXpVlvExPutlIsbtVZSsSlE
+lFV5uuVOVYcfjnQJtuUj5JtwP6mhn0Njj/YiJPzG2ugpM0M=
+=BDYe
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c8f18a2..f923e74 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -218,6 +218,7 @@
<trusting group="com.squareup.leakcanary"/>
</trusted-key>
<trusted-key id="62c82e50836eb3ee" group="com.github.gundy"/>
+ <trusted-key id="635ee627345f3c1dd422b2e207d3516820bcf6b1" group="com.github.ben-manes.caffeine"/>
<trusted-key id="6525fd70cc303655" group="org.codehaus.mojo"/>
<trusted-key id="666a4692ce11b7b3f4eb7b3410066a9707090cf9" group="org.javassist" name="javassist"/>
<trusted-key id="694621a7227d8d5289699830abe9f3126bb741c1">
@@ -257,6 +258,7 @@
<trusted-key id="79156e0351af8604de9b186b09a79e1e15a04694" group="org.vafer" name="jdependency"/>
<trusted-key id="7999befba1039e8b" group="net.bytebuddy"/>
<trusted-key id="7a8860944fad5f62" group="org.apache.commons"/>
+ <trusted-key id="7c669810892cbd3148fa92995b05ccde140c2876" group="org.eclipse.jgit"/>
<trusted-key id="7c7d8456294423ba" group="org.objenesis"/>
<trusted-key id="7cb548acfe3d47e92afa566dc29b11246382a4d7" group="com.charleskorn.kaml"/>
<trusted-key id="7cd52b5a8295137c88fb5748dddafa7674e54418" group="org.testng" name="testng"/>
@@ -373,6 +375,8 @@
<trusted-key id="c6f7d1c804c821f49af3bfc13ad93c3c677a106e" group="io.perfmark" name="perfmark-api"/>
<trusted-key id="c70b844f002f21f6d2b9c87522e44ac0622b91c3" group="com.beust" name="jcommander"/>
<trusted-key id="c7be5bcc9fec15518cfda882b0f3710fa64900e7">
+ <trusting group="com.google.auto"/>
+ <trusting group="com.google.auto.service"/>
<trusting group="com.google.auto.value"/>
<trusting group="com.google.code.gson"/>
</trusted-key>
@@ -398,6 +402,7 @@
<trusted-key id="cfae163b64ac9189" group="org.jetbrains.skiko"/>
<trusted-key id="d041cad2e452550f" group="com.google.protobuf"/>
<trusted-key id="d196a5e3e70732eeb2e5007f1861c322c56014b2" group="commons-lang"/>
+ <trusted-key id="d477d51812e692011db11e66a6ea2e2bf22e0543" group="io.github.java-diff-utils"/>
<trusted-key id="d4c89ea4aaf455fd88b22087efe8086f9e93774e" group="junit"/>
<trusted-key id="d4da5eab3cd7e958" group="com.google.devtools.ksp"/>
<trusted-key id="d4fb0b7b5e8c18c993a8a386eb9d04a9a679fe18" group="com.uber.nullaway" name="nullaway"/>
@@ -413,6 +418,7 @@
<trusted-key id="dddafa7674e54418" group="org.testng"/>
<trusted-key id="e0130a3ed5a2079e" group="org.webjars"/>
<trusted-key id="e0cb7823cfd00fbf" group="com.jakewharton.android.repackaged"/>
+ <trusted-key id="e0d98c5fd55a8af232290e58dee12b9896f97e34" group="org.pcollections"/>
<trusted-key id="e16ab52d79fd224f" group="com.google.api.grpc"/>
<trusted-key id="e62231331bca7e1f292c9b88c1b12a5d99c0729d" group="org.jetbrains"/>
<trusted-key id="e77417ac194160a3fabd04969a259c7ee636c5ed">
@@ -427,6 +433,7 @@
<trusted-key id="eb380dc13c39f675" group="com.intellij"/>
<trusted-key id="eb9d04a9a679fe18" group="com.uber.nullaway"/>
<trusted-key id="ecdfea3cb4493b94" group="jline"/>
+ <trusted-key id="ee0ca873074092f806f59b65d364abaa39a47320" group="com.google.errorprone"/>
<trusted-key id="ee9e7dc9d92fc896" group="com.google.errorprone"/>
<trusted-key id="eef9ecc7d5d90518" group="com.google.dagger"/>
<trusted-key id="efe8086f9e93774e" group="junit"/>
@@ -461,8 +468,7 @@
</trusted-keys>
</configuration>
<components>
- <!-- Unsigned -->
- <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1">
+ <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1" androidx:reason="Unsigned">
<artifact name="backport-util-concurrent-3.1.jar">
<sha256 value="f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902" origin="Generated by Gradle"/>
</artifact>
@@ -470,8 +476,7 @@
<sha256 value="770471090ca40a17b9e436ee2ec00819be42042da6f4085ece1d37916dc08ff9" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="classworlds" name="classworlds" version="1.1-alpha-2">
+ <component group="classworlds" name="classworlds" version="1.1-alpha-2" androidx:reason="Unsigned">
<artifact name="classworlds-1.1-alpha-2.jar">
<sha256 value="2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3" origin="Generated by Gradle"/>
</artifact>
@@ -479,8 +484,7 @@
<sha256 value="0cc647963b74ad1d7a37c9868e9e5a8f474e49297e1863582253a08a4c719cb1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gundy/semver4j/issues/6 -->
- <component group="com.github.gundy" name="semver4j" version="0.16.4">
+ <component group="com.github.gundy" name="semver4j" version="0.16.4" androidx:reason="Unsigned https://github.com/gundy/semver4j/issues/6">
<artifact name="semver4j-0.16.4-nodeps.jar">
<sha256 value="3f59eca516374ccd4fd3551625bf50f8a4b191f700508f7ce4866460a6128af0" origin="Generated by Gradle"/>
</artifact>
@@ -489,8 +493,7 @@
<sha256 value="32001db2443b339dd21f5b79ff29d1ade722d1ba080c214bde819f0f72d1604d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google" name="google" version="1">
+ <component group="com.google" name="google" version="1" androidx:reason="Unsigned">
<artifact name="google-1.pom">
<sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle"/>
</artifact>
@@ -543,8 +546,7 @@
<sha256 value="c6898b1f71e69b15bf90c31fc3ef2de1cffbf454a770700f755b5a47ea48b540" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.code.findbugs" name="jsr305" version="1.3.9">
+ <component group="com.google.code.findbugs" name="jsr305" version="1.3.9" androidx:reason="Unsigned">
<artifact name="jsr305-1.3.9.jar">
<sha256 value="905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed" origin="Generated by Gradle"/>
</artifact>
@@ -552,8 +554,7 @@
<sha256 value="feab9191311c3d7aeef2b66d6064afc80d3d1d52d980fb07ae43c78c987ba93a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.code.findbugs" name="jsr305" version="2.0.1">
+ <component group="com.google.code.findbugs" name="jsr305" version="2.0.1" androidx:reason="Unsigned">
<artifact name="jsr305-2.0.1.jar">
<sha256 value="1e7f53fa5b8b5c807e986ba335665da03f18d660802d8bf061823089d1bee468" origin="Generated by Gradle"/>
</artifact>
@@ -561,8 +562,7 @@
<sha256 value="02c12c3c2ae12dd475219ff691c82a4d9ea21f44bc594a181295bf6d43dcfbb0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.prefab" name="cli" version="2.0.0">
+ <component group="com.google.prefab" name="cli" version="2.0.0" androidx:reason="Unsigned">
<artifact name="cli-2.0.0-all.jar">
<sha256 value="d9bd89f68446b82be038aae774771ad85922d0b375209b17625a2734b5317e29" origin="Generated by Gradle"/>
</artifact>
@@ -570,8 +570,7 @@
<sha256 value="4856401a263b39c5394b36a16e0d99628cf05c68008a0cda9691c72bb101e1df" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.googlecode.json-simple" name="json-simple" version="1.1">
+ <component group="com.googlecode.json-simple" name="json-simple" version="1.1" androidx:reason="Unsigned">
<artifact name="json-simple-1.1.jar">
<sha256 value="2d9484f4c649f708f47f9a479465fc729770ee65617dca3011836602264f6439" origin="Generated by Gradle"/>
</artifact>
@@ -579,20 +578,17 @@
<sha256 value="47a89be0fa0fedd476db5fd2c83487654d2a119c391f83a142be876667cf7dab" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
- <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2">
+ <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
<artifact name="common-custom-user-data-gradle-plugin-1.7.2.pom">
<sha256 value="c70db912c8b127b1b9a6c0cccac1a9353e9fc3b063a3be0114a5208f43c09c31" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
- <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2">
+ <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
<artifact name="gradle-enterprise-gradle-plugin-3.10.2.pom">
<sha256 value="57603c9a75a9ef86ce30b1cb2db728d3cd9caf1be967343f1fc2316c85df5653" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.okio" name="okio" version="2.8.0">
+ <component group="com.squareup.okio" name="okio" version="2.8.0" androidx:reason="Unsigned">
<artifact name="okio-2.8.0.module">
<sha256 value="17baab7270389a5fa63ab12811864d0a00f381611bc4eb042fa1bd5918ed0965" origin="Generated by Gradle"/>
</artifact>
@@ -600,20 +596,17 @@
<sha256 value="4496b06e73982fcdd8a5393f46e5df2ce2fa4465df5895454cac68a32f09bbc8" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.okio" name="okio" version="2.10.0">
+ <component group="com.squareup.okio" name="okio" version="2.10.0" androidx:reason="Unsigned">
<artifact name="okio-jvm-2.10.0.jar">
<sha256 value="a27f091d34aa452e37227e2cfa85809f29012a8ef2501a9b5a125a978e4fcbc1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0">
+ <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0" androidx:reason="Unsigned">
<artifact name="sqldelight-coroutines-extensions-jvm-1.3.0.jar">
<sha256 value="47305eab44f8b2aef533d8ce76cec9eb5175715cac26b538b6bff5b106ed0ba1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-grpc-client-3.6.0.module">
<sha256 value="f4d91b43e5ce4603d63842652f063f16c0827abda1922dfb9551a4ac23ba4462" origin="Generated by Gradle"/>
</artifact>
@@ -621,8 +614,7 @@
<sha256 value="96904172b35af353e4459786a7d02f1550698cd03b249799ecb563cea3b4c277" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-runtime" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-runtime" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-runtime-3.6.0.module">
<sha256 value="3b99891842fdec80e7b24ae7f7c485ae41ca35b47c902ca2043cc948aaf58010" origin="Generated by Gradle"/>
</artifact>
@@ -630,8 +622,7 @@
<sha256 value="ac41d3f9b8a88046788c6827b0519bf0c53dcc271f598f48aa666c6f5a9523d0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-schema" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-schema" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-schema-3.6.0.module">
<sha256 value="85abd765f2efca0545889c935d8c240e31736a22221231a59bcc4510358b6aaa" origin="Generated by Gradle"/>
</artifact>
@@ -639,8 +630,7 @@
<sha256 value="108bc4bafe7024a41460a1a60e72b6a95b69e5afd29c9f11ba7d8e0de2207976" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187 -->
- <component group="de.undercouch" name="gradle-download-task" version="4.1.1">
+ <component group="de.undercouch" name="gradle-download-task" version="4.1.1" androidx:reason="Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187">
<artifact name="gradle-download-task-4.1.1.jar">
<ignored-keys>
<ignored-key id="1fa37fbe4453c1073e7ef61d6449005f96bc97a3" reason="PGP verification failed"/>
@@ -658,8 +648,7 @@
</sha256>
</artifact>
</component>
- <!-- Unsigned https://github.com/johnrengelman/shadow/issues/760 -->
- <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1">
+ <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1" androidx:reason="Unsigned https://github.com/johnrengelman/shadow/issues/760">
<artifact name="shadow-7.1.1.jar">
<sha256 value="a870861a7a3d54ffd97822051a27b2f1b86dd5c480317f0b97f3b27581b742af" origin="Generated by Gradle"/>
</artifact>
@@ -667,8 +656,7 @@
<sha256 value="683be0cd32af9c80a6d4a143b9a6ac2eb45ebc3ccd16db4ca11b94e55fc5e52f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13">
+ <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13" androidx:reason="Unsigned">
<artifact name="protobuf-gradle-plugin-0.8.13.jar">
<sha256 value="8a04b6eee4eab68c73b6e61cc8e00206753691b781d042afbae746f97e8c6f2d" origin="Generated by Gradle"/>
</artifact>
@@ -676,8 +664,7 @@
<sha256 value="d8c46016037cda6360561b9c6a21a6c2a4847cad15c3c63903e15328fbcccc45" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.activation" name="activation" version="1.1">
+ <component group="javax.activation" name="activation" version="1.1" androidx:reason="Unsigned">
<artifact name="activation-1.1.jar">
<sha256 value="2881c79c9d6ef01c58e62beea13e9d1ac8b8baa16f2fc198ad6e6776defdcdd3" origin="Generated by Gradle"/>
</artifact>
@@ -685,8 +672,7 @@
<sha256 value="d490e540a11504b9d71718b1c85fef7b3de6802361290824539b076d58faa8a0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.annotation" name="jsr250-api" version="1.0">
+ <component group="javax.annotation" name="jsr250-api" version="1.0" androidx:reason="Unsigned">
<artifact name="jsr250-api-1.0.jar">
<sha256 value="a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f" origin="Generated by Gradle"/>
</artifact>
@@ -694,8 +680,7 @@
<sha256 value="548b0ef6f04356ef2283af5140d9404f38fd3891a509d468537abf2f9462944d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.inject" name="javax.inject" version="1">
+ <component group="javax.inject" name="javax.inject" version="1" androidx:reason="Unsigned">
<artifact name="javax.inject-1.jar">
<sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/>
</artifact>
@@ -703,8 +688,7 @@
<sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.xml.stream" name="stax-api" version="1.0-2">
+ <component group="javax.xml.stream" name="stax-api" version="1.0-2" androidx:reason="Unsigned">
<artifact name="stax-api-1.0-2.jar">
<sha256 value="e8c70ebd76f982c9582a82ef82cf6ce14a7d58a4a4dca5cb7b7fc988c80089b7" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
@@ -712,8 +696,7 @@
<sha256 value="2864f19da84fd52763d75a197a71779b2decbccaac3eb4e4760ffc884c5af4a2" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9">
+ <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9" androidx:reason="Unsigned">
<artifact name="japicmp-gradle-plugin-0.2.9.jar">
<sha256 value="320944e8f3a42a38a5e0f08c6e1e8ae11a63fc82e1f7bf0429a6b7d89d26fac3" origin="Generated by Gradle"/>
</artifact>
@@ -721,8 +704,7 @@
<sha256 value="41fc0c243907c241cffa24a06a8cb542747c848ebad5feb6b0413d61b4a0ebc2" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="nekohtml" name="nekohtml" version="1.9.6.2">
+ <component group="nekohtml" name="nekohtml" version="1.9.6.2" androidx:reason="Unsigned">
<artifact name="nekohtml-1.9.6.2.jar">
<sha256 value="fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09" origin="Generated by Gradle"/>
</artifact>
@@ -730,8 +712,7 @@
<sha256 value="f5655d331af6afcd4dbaedaa739b889380c771a7e83f7aea5c8544a05074cf0b" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="nekohtml" name="xercesMinimal" version="1.9.6.2">
+ <component group="nekohtml" name="xercesMinimal" version="1.9.6.2" androidx:reason="Unsigned">
<artifact name="xercesMinimal-1.9.6.2.jar">
<sha256 value="95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438" origin="Generated by Gradle"/>
</artifact>
@@ -739,20 +720,17 @@
<sha256 value="c219d697fa9c8f243d8f6e347499b6d4e8af1d0cac4bbc7b3907d338a2024c13" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="1">
+ <component group="net.java" name="jvnet-parent" version="1" androidx:reason="Unsigned">
<artifact name="jvnet-parent-1.pom">
<sha256 value="281440811268e65d9e266b3cc898297e214e04f09740d0386ceeb4a8923d63bf" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="3">
+ <component group="net.java" name="jvnet-parent" version="3" androidx:reason="Unsigned">
<artifact name="jvnet-parent-3.pom">
<sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="4">
+ <component group="net.java" name="jvnet-parent" version="4" androidx:reason="Unsigned">
<artifact name="jvnet-parent-4.pom">
<sha256 value="471395735549495297c8ff939b9a32e08b91302020ff773586d27e497abb8fbb" origin="Generated by Gradle"/>
<!-- Gradle doesn't add keyring files for parent poms so we need to explicitly specify it here to trust -->
@@ -760,14 +738,12 @@
<pgp value="44fbdbbc1a00fe414f1c1873586654072ead6677"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="5">
+ <component group="net.java" name="jvnet-parent" version="5" androidx:reason="Unsigned">
<artifact name="jvnet-parent-5.pom">
<sha256 value="1af699f8d9ddab67f9a0d202fbd7915eb0362a5a6dfd5ffc54cafa3465c9cb0a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.sf.kxml" name="kxml2" version="2.3.0">
+ <component group="net.sf.kxml" name="kxml2" version="2.3.0" androidx:reason="Unsigned">
<artifact name="kxml2-2.3.0.jar">
<sha256 value="f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2" origin="Generated by Gradle"/>
</artifact>
@@ -775,8 +751,7 @@
<sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2">
+ <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2" androidx:reason="Unsigned">
<artifact name="tagsoup-1.2.jar">
<sha256 value="10d12b82c9a58a7842765a1152a56fbbd11eac9122a621f5a86a087503297266" origin="Generated by Gradle"/>
</artifact>
@@ -784,8 +759,7 @@
<sha256 value="186fd460ee13150e31188703a2c871bf86e20332636f3ede4ab959cd5568da78" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15">
+ <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15" androidx:reason="Unsigned">
<artifact name="plexus-utils-1.5.15.jar">
<sha256 value="2ca121831e597b4d8f2cb22d17c5c041fc23a7777ceb6bfbdd4dfb34bbe7d997" origin="Generated by Gradle"/>
</artifact>
@@ -793,26 +767,22 @@
<sha256 value="12a3c9a32b82fdc95223cab1f9d344e14ef3e396da14c4d0013451646f3280e7" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus" version="1.0.4">
+ <component group="org.codehaus.plexus" name="plexus" version="1.0.4" androidx:reason="Unsigned">
<artifact name="plexus-1.0.4.pom">
<sha256 value="2242fd02d12b1ca73267fb3d89863025517200e7a4ee426cba4667d0172c74c3" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus" version="2.0.2">
+ <component group="org.codehaus.plexus" name="plexus" version="2.0.2" androidx:reason="Unsigned">
<artifact name="plexus-2.0.2.pom">
<sha256 value="e246e2a062b5d989fdefc521c9c56431ba5554ff8d2344edee9218a34a546a33" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14">
+ <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14" androidx:reason="Unsigned">
<artifact name="plexus-components-1.1.14.pom">
<sha256 value="381d72c526be217b770f9f8c3f749a86d3b1548ac5c1fcb48d267530ec60d43f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1">
+ <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1" androidx:reason="Unsigned">
<artifact name="plexus-container-default-1.0-alpha-9-stable-1.jar">
<sha256 value="7c758612888782ccfe376823aee7cdcc7e0cdafb097f7ef50295a0b0c3a16edf" origin="Generated by Gradle"/>
</artifact>
@@ -820,14 +790,12 @@
<sha256 value="ef71d45a49edfe76be0f520312a76bc2aae73ec0743a5ffffe10d30122c6e2b2" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3">
+ <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3" androidx:reason="Unsigned">
<artifact name="plexus-containers-1.0.3.pom">
<sha256 value="7c75075badcb014443ee94c8c4cad2f4a9905be3ce9430fe7b220afc7fa3a80f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11">
+ <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11" androidx:reason="Unsigned">
<artifact name="plexus-interpolation-1.11.jar">
<sha256 value="fd9507feb858fa620d1b4aa4b7039fdea1a77e09d3fd28cfbddfff468d9d8c28" origin="Generated by Gradle"/>
</artifact>
@@ -835,8 +803,7 @@
<sha256 value="b84d281f59b9da528139e0752a0e1cab0bd98d52c58442b00e45c9748e1d9eee" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-android-gradle-plugin-0.9.17-g014.jar">
<sha256 value="64b2e96fd20762351c74f08d598d49c25a490a3b685b8a09446e81d6db36fe81" origin="Generated by Gradle"/>
</artifact>
@@ -844,8 +811,7 @@
<sha256 value="956ff381c6c775161a82823bb52d0aa40a8f6a37ab85059f149531f5e5efb7da" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-fatjar-0.9.17-g014.jar">
<sha256 value="47cf09501402a101e555588cf5fa9ed83f8572bce9fd60db29e74b5d079628e3" origin="Generated by Gradle"/>
</artifact>
@@ -853,8 +819,7 @@
<sha256 value="ceb601f55f14337261fea474bb061407dc0e52146f80d74cd0b43d66febd401f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-gradle-plugin-0.9.17-g014.jar">
<sha256 value="643a7eddeb521832c6021508b7477b603517438481bc06633dca12eb1f339422" origin="Generated by Gradle"/>
</artifact>
@@ -867,8 +832,7 @@
<sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm" version="7.0">
+ <component group="org.ow2.asm" name="asm" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-7.0.jar">
<sha256 value="b88ef66468b3c978ad0c97fd6e90979e56155b4ac69089ba7a44e9aa7ffe9acf" origin="Generated by Gradle"/>
</artifact>
@@ -876,8 +840,7 @@
<sha256 value="83f65b1083d5ce4f8ba7f9545cfe9ff17824589c9a7cc82c3a4695801e4f5f68" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-analysis" version="7.0">
+ <component group="org.ow2.asm" name="asm-analysis" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-analysis-7.0.jar">
<sha256 value="e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474" origin="Generated by Gradle"/>
</artifact>
@@ -885,8 +848,7 @@
<sha256 value="c6b54477e9d5bae1e7addff2e24cbf92aaff2ff08fd6bc0596c3933c3fadc2cb" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-commons" version="7.0">
+ <component group="org.ow2.asm" name="asm-commons" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-commons-7.0.jar">
<sha256 value="fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d" origin="Generated by Gradle"/>
</artifact>
@@ -894,8 +856,7 @@
<sha256 value="f4c697886cdb4a5b2472054a0b5e34371e9b48e620be40c3ed48e1f4b6d51eb4" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-tree" version="7.0">
+ <component group="org.ow2.asm" name="asm-tree" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-tree-7.0.jar">
<sha256 value="cfd7a0874f9de36a999c127feeadfbfe6e04d4a71ee954d7af3d853f0be48a6c" origin="Generated by Gradle"/>
</artifact>
@@ -903,8 +864,7 @@
<sha256 value="d39e7dd12f4ff535a0839d1949c39c7644355a4470220c94b76a5c168c57a068" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-util" version="7.0">
+ <component group="org.ow2.asm" name="asm-util" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-util-7.0.jar">
<sha256 value="75fbbca440ef463f41c2b0ab1a80abe67e910ac486da60a7863cbcb5bae7e145" origin="Generated by Gradle"/>
</artifact>
@@ -912,14 +872,12 @@
<sha256 value="e07bce4bb55d5a06f4c10d912fc9dee8a9b9c04ec549bbb8db4f20db34706f75" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.sonatype.oss" name="oss-parent" version="7">
+ <component group="org.sonatype.oss" name="oss-parent" version="7" androidx:reason="Unsigned">
<artifact name="oss-parent-7.pom">
<sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Invalid signature -->
- <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2">
+ <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2" androidx:reason="Invalid signature">
<artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar">
<pgp value="db0597e3144342256bc81e3ec727d053c4481cf5"/>
<sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
@@ -933,8 +891,7 @@
</sha256>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="pull-parser" name="pull-parser" version="2">
+ <component group="pull-parser" name="pull-parser" version="2" androidx:reason="Unsigned">
<artifact name="pull-parser-2.jar">
<sha256 value="b20c1e56513faeffb9b01d9d03ba1a36128ac3f9be39b2d0edbe2e240b029d3f" origin="Generated by Gradle"/>
</artifact>
@@ -942,8 +899,7 @@
<sha256 value="4823677670797c2b71e8ebbe5437c41151f4e8edb7c6c0d473279715070f36d3" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="xmlpull" name="xmlpull" version="1.1.3.1">
+ <component group="xmlpull" name="xmlpull" version="1.1.3.1" androidx:reason="Unsigned">
<artifact name="xmlpull-1.1.3.1.jar">
<sha256 value="34e08ee62116071cbb69c0ed70d15a7a5b208d62798c59f2120bb8929324cb63" origin="Generated by Gradle"/>
</artifact>
@@ -951,8 +907,7 @@
<sha256 value="8f10ffd8df0d3e9819c8cc8402709c6b248bc53a954ef6e45470d9ae3a5735fb" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="xpp3" name="xpp3" version="1.1.4c">
+ <component group="xpp3" name="xpp3" version="1.1.4c" androidx:reason="Unsigned">
<artifact name="xpp3-1.1.4c.jar">
<sha256 value="0341395a481bb887803957145a6a37879853dd625e9244c2ea2509d9bb7531b9" origin="Generated by Gradle"/>
</artifact>
@@ -960,8 +915,7 @@
<sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned, b/227204920 -->
- <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10">
+ <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10" androidx:reason="Unsigned, b/227204920">
<artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz">
<sha256 value="f3bd13bc0089fe95609109604d5993a49838828787f15e0e79eef6612b587dc1" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz"/>
</artifact>
diff --git a/graphics/OWNERS b/graphics/OWNERS
index db046a2..9ba9c32 100644
--- a/graphics/OWNERS
+++ b/graphics/OWNERS
@@ -1,5 +1,4 @@
# Bug component: 1137062
sumir@google.com
jreck@google.com
-xxayedawgxx@google.com
-njawad@google.com
\ No newline at end of file
+njawad@google.com
diff --git a/javascriptengine/OWNERS b/javascriptengine/OWNERS
new file mode 100644
index 0000000..e3fd7e0
--- /dev/null
+++ b/javascriptengine/OWNERS
@@ -0,0 +1,2 @@
+abhijithnair@google.com
+torne@google.com
diff --git a/javascriptengine/javascriptengine/api/current.txt b/javascriptengine/javascriptengine/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/res-current.txt b/javascriptengine/javascriptengine/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/res-current.txt
diff --git a/javascriptengine/javascriptengine/api/restricted_current.txt b/javascriptengine/javascriptengine/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/build.gradle b/javascriptengine/javascriptengine/build.gradle
new file mode 100644
index 0000000..69ad2ab
--- /dev/null
+++ b/javascriptengine/javascriptengine/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ annotationProcessor(libs.nullaway)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.javascriptengine"
+}
+
+androidx {
+ name = "JavaScript Engine"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.JAVASCRIPTENGINE
+ inceptionYear = "2022"
+ description = "Javascript Engine is a static library you can add to your Android application in order to evaluate JavaScript."
+}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
similarity index 67%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
rename to javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
index fdc9e2d..4082445 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,12 +14,8 @@
* limitations under the License.
*/
-package androidx.camera.integration.uiwidgets.compose.ui.screen.gallery
-
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Composable
-fun GalleryScreen() {
- Text("Gallery Screen")
-}
\ No newline at end of file
+/**
+ * The androidx.javascriptengine library is a static library you can add to your Android application
+ * in order to evaluate JavaScript.
+ */
+package androidx.javascriptengine;
diff --git a/libraryversions.toml b/libraryversions.toml
index 4895850..5c0c365 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -19,7 +19,7 @@
CAR_APP = "1.3.0-alpha01"
COLLECTION = "1.3.0-alpha02"
COMPOSE = "1.3.0-alpha02"
-COMPOSE_COMPILER = "1.3.0-beta01"
+COMPOSE_COMPILER = "1.3.0-rc01"
COMPOSE_MATERIAL3 = "1.0.0-alpha15"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha01"
CONTENTPAGER = "1.1.0-alpha01"
@@ -61,6 +61,7 @@
HILT_NAVIGATION_COMPOSE = "1.1.0-alpha01"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
+JAVASCRIPTENGINE = "1.0.0-alpha01"
LEANBACK = "1.2.0-alpha03"
LEANBACK_GRID = "1.0.0-alpha02"
LEANBACK_PAGING = "1.1.0-alpha10"
@@ -74,7 +75,7 @@
MEDIA = "1.7.0-alpha01"
MEDIA2 = "1.3.0-alpha01"
MEDIAROUTER = "1.4.0-alpha01"
-METRICS = "1.0.0-alpha03"
+METRICS = "1.0.0-alpha04"
NAVIGATION = "2.6.0-alpha01"
PAGING = "3.2.0-alpha02"
PAGING_COMPOSE = "1.0.0-alpha16"
@@ -188,6 +189,7 @@
INSPECTION = { group = "androidx.inspection", atomicGroupVersion = "versions.INSPECTION" }
INSPECTION_EXTENSIONS = { group = "androidx.inspection.extensions", atomicGroupVersion = "versions.SQLITE_INSPECTOR" }
INTERPOLATOR = { group = "androidx.interpolator", atomicGroupVersion = "versions.INTERPOLATOR" }
+JAVASCRIPTENGINE = { group = "androidx.javascriptengine", atomicGroupVersion = "versions.JAVASCRIPTENGINE" }
LEANBACK = { group = "androidx.leanback" }
LEGACY = { group = "androidx.legacy" }
LIBYUV = { group = "libyuv", atomicGroupVersion = "versions.LIBYUV" }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 1ec1fe6b..8bfb087 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -48,7 +48,7 @@
* Temporary per-frame to track UI and user state.
* Unlike the states tracked in `states`, any state in this structure is only valid until
* the next frame, at which point it is cleared. Any state data added here is automatically
- * removed; there is no matching "remove" method for [.addSingleFrameState]
+ * removed; there is no matching "remove" method for [.putSingleFrameState]
*
* @see putSingleFrameState
*/
@@ -164,7 +164,13 @@
* State information can be about UI elements that are currently active (such as the current
* [Activity] or layout) or a user interaction like flinging a list.
* If the PerformanceMetricsState object already contains an entry with the same key,
- * the old value is replaced by the new one.
+ * the old value is replaced by the new one. Note that this means apps with several
+ * instances of similar objects (such as multipe `RecyclerView`s) should
+ * therefore use unique keys for these instances to avoid clobbering state values
+ * for other instances and to provide enough information for later analysis which
+ * allows for disambiguation between these objects. For example, using "RVHeaders" and
+ * "RVContent" might be more helpful than just "RecyclerView" for a messaging app using
+ * `RecyclerView` objects for both a headers list and a list of message contents.
*
* Some state may be provided automatically by other AndroidX libraries.
* But applications are encouraged to add user state specific to those applications
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c872393..8bc8969 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -32,10 +32,10 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.collection:collection-ktx:1.1.0")
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index 10d30a9..77dcb58 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -28,6 +28,7 @@
import androidx.collection.valueIterator
import androidx.core.content.res.use
import androidx.navigation.common.R
+import java.util.regex.Pattern
import kotlin.reflect.KClass
/**
@@ -508,6 +509,50 @@
return defaultArgs
}
+ /**
+ * Parses a dynamic label containing arguments into a String.
+ *
+ * Supports String Resource arguments by parsing `R.string` values of `ReferenceType`
+ * arguments found in `android:label` into their String values.
+ *
+ * Returns `null` if label is null.
+ *
+ * Returns the original label if the label was a static string.
+ *
+ * @param context Context used to resolve a resource's name
+ * @param bundle Bundle containing the arguments used in the label
+ * @return The parsed string or null if the label is null
+ * @throws IllegalArgumentException if an argument provided in the label cannot be found in
+ * the bundle, or if the label contains a string template but the bundle is null
+ */
+ public fun fillInLabel(context: Context, bundle: Bundle?): String? {
+ val label = label ?: return null
+
+ val fillInPattern = Pattern.compile("\\{(.+?)\\}")
+ val matcher = fillInPattern.matcher(label)
+ val builder = StringBuffer()
+
+ while (matcher.find()) {
+ val argName = matcher.group(1)
+ if (bundle != null && bundle.containsKey(argName)) {
+ matcher.appendReplacement(builder, "")
+ val argType = argName?.let { arguments[argName]?.type }
+ if (argType == NavType.ReferenceType) {
+ val value = context.getString(bundle.getInt(argName))
+ builder.append(value)
+ } else {
+ builder.append(bundle.getString(argName))
+ }
+ } else {
+ throw IllegalArgumentException(
+ "Could not find \"$argName\" in $bundle to fill label \"$label\""
+ )
+ }
+ }
+ matcher.appendTail(builder)
+ return builder.toString()
+ }
+
override fun toString(): String {
val sb = StringBuilder()
sb.append(javaClass.simpleName)
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index f26a72d..3d7416c 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,18 +28,18 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.1")
- api("androidx.activity:activity-compose:1.5.0")
+ api("androidx.activity:activity-compose:1.5.1")
api("androidx.compose.animation:animation:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
- api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation "androidx.lifecycle:lifecycle-common-java8:2.5.0"
+ implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 9a64a90..12ced4a 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- api("androidx.fragment:fragment-ktx:1.5.0")
+ api("androidx.fragment:fragment-ktx:1.5.1")
api(project(":navigation:navigation-runtime"))
api("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 6478105..94c45c4 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -25,9 +25,9 @@
dependencies {
api(project(":navigation:navigation-common"))
- api("androidx.activity:activity-ktx:1.5.0")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.activity:activity-ktx:1.5.1")
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.annotation:annotation-experimental:1.1.0")
implementation('androidx.collection:collection:1.0.0')
diff --git a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
index 7c4c68e..356161d 100644
--- a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
+++ b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
@@ -17,8 +17,12 @@
package androidx.navigation.ui
import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.core.content.res.TypedArrayUtils.getString
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.NavHostController
@@ -51,7 +55,7 @@
@UiThreadTest
@Test
- fun navigateWithStringReferenceArgs() {
+ fun navigateWithSingleStringReferenceArg() {
val context = ApplicationProvider.getApplicationContext<Context>()
val navController = NavHostController(context)
navController.navigatorProvider.addNavigator(TestNavigator())
@@ -74,7 +78,155 @@
endDestination + "/${R.string.dest_title}"
)
- val expected = context.resources.getString(R.string.dest_title)
+ val expected = "${context.resources.getString(R.string.dest_title)}"
assertThat(toolbar.title.toString()).isEqualTo(expected)
}
+
+ @UiThreadTest
+ @Test
+ fun navigateWithMultiStringReferenceArgs() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test("$endDestination/{test}") {
+ label = "start/{test}/end/{test}"
+ argument(name = "test") {
+ type = NavType.ReferenceType
+ }
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+ navController.navigate(
+ endDestination + "/${R.string.dest_title}"
+ )
+
+ val argString = context.resources.getString(R.string.dest_title)
+ val expected = "start/$argString/end/$argString"
+ assertThat(toolbar.title.toString()).isEqualTo(expected)
+ }
+
+ @UiThreadTest
+ @Test(expected = IllegalArgumentException::class)
+ fun navigateWithArg_NotFoundInBundleThrows() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/{test}"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test(endDestination) {
+ label = labelString
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // empty bundle
+ val testListener = createToolbarOnDestinationChangedListener(
+ toolbar = toolbar, bundle = Bundle(), context = context, navController = navController
+ )
+
+ // navigate to destination. Since the argument {test} is not present in the bundle,
+ // this should throw an IllegalArgumentException
+ navController.apply {
+ addOnDestinationChangedListener(testListener)
+ navigate(route = endDestination)
+ }
+ }
+
+ @UiThreadTest
+ @Test(expected = IllegalArgumentException::class)
+ fun navigateWithArg_NullBundleThrows() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/{test}"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test("$endDestination/{test}") {
+ label = labelString
+ argument(name = "test") {
+ type = NavType.ReferenceType
+ }
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // null Bundle
+ val testListener = createToolbarOnDestinationChangedListener(
+ toolbar = toolbar, bundle = null, context = context, navController = navController
+ )
+
+ // navigate to destination, should throw due to template found but null bundle
+ navController.apply {
+ addOnDestinationChangedListener(testListener)
+ navigate(route = endDestination + "/${R.string.dest_title}")
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun navigateWithStaticLabel() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/test"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test(endDestination) {
+ label = labelString
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // navigate to destination, static label should be returned directly
+ navController.navigate(route = endDestination)
+ assertThat(toolbar.title.toString()).isEqualTo(labelString)
+ }
+
+ private fun createToolbarOnDestinationChangedListener(
+ toolbar: Toolbar,
+ bundle: Bundle?,
+ context: Context,
+ navController: NavController
+ ): NavController.OnDestinationChangedListener {
+ return object : AbstractAppBarOnDestinationChangedListener(
+ context, AppBarConfiguration.Builder(navController.graph).build()
+ ) {
+ override fun setTitle(title: CharSequence?) {
+ toolbar.title = title
+ }
+
+ override fun onDestinationChanged(
+ controller: NavController,
+ destination: NavDestination,
+ arguments: Bundle?
+ ) {
+ super.onDestinationChanged(controller, destination, bundle)
+ }
+
+ override fun setNavigationIcon(icon: Drawable?, contentDescription: Int) {}
+ }
+ }
}
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
index 2c73ae9..9962d7b 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
@@ -26,10 +26,8 @@
import androidx.navigation.FloatingWindow
import androidx.navigation.NavController
import androidx.navigation.NavDestination
-import androidx.navigation.NavType
import androidx.navigation.ui.NavigationUI.matchDestinations
import java.lang.ref.WeakReference
-import java.util.regex.Pattern
/**
* The abstract OnDestinationChangedListener for keeping any type of app bar updated.
@@ -51,7 +49,6 @@
protected abstract fun setNavigationIcon(icon: Drawable?, @StringRes contentDescription: Int)
- @Suppress("DEPRECATION")
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
@@ -65,33 +62,12 @@
controller.removeOnDestinationChangedListener(this)
return
}
- val label = destination.label
- if (label != null) {
- // Fill in the data pattern with the args to build a valid URI
- val title = StringBuffer()
- val fillInPattern = Pattern.compile("\\{(.+?)\\}")
- val matcher = fillInPattern.matcher(label)
- while (matcher.find()) {
- val argName = matcher.group(1)
- if (arguments != null && arguments.containsKey(argName)) {
- matcher.appendReplacement(title, "")
- val argType = argName?.let { destination.arguments[argName]?.type }
- if (argType == NavType.ReferenceType) {
- val value = context.resources.getString(arguments[argName] as Int)
- title.append(value)
- } else {
- title.append(arguments[argName].toString())
- }
- } else {
- throw IllegalArgumentException(
- "Could not find \"$argName\" in $arguments to fill label \"$label\""
- )
- }
- }
- matcher.appendTail(title)
- setTitle(title)
+ val label = destination.fillInLabel(context, arguments)
+ if (label != null) {
+ setTitle(label)
}
+
val isTopLevelDestination = destination.matchDestinations(topLevelDestinations)
if (openableLayout == null && isTopLevelDestination) {
setNavigationIcon(null, 0)
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index fdc7e4b..8f4aacb5 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -23,6 +23,11 @@
}
dependencies {
+ // Atomic Group
+ constraints {
+ implementation(project(":paging:paging-runtime"))
+ }
+
api("androidx.annotation:annotation:1.3.0")
api("androidx.arch.core:core-common:2.1.0")
api(libs.kotlinStdlib)
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e7cc847..eb2de3b 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -31,6 +31,11 @@
}
dependencies {
+ //Atomic Group
+ constraints {
+ implementation(project(":paging:paging-common"))
+ }
+
api(project(":paging:paging-common"))
// Ensure that the -ktx dependency graph mirrors the Java dependency graph
api(project(":paging:paging-common-ktx"))
diff --git a/recyclerview/recyclerview/api/1.3.0-beta02.txt b/recyclerview/recyclerview/api/1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/current.ignore b/recyclerview/recyclerview/api/current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/current.ignore
+++ b/recyclerview/recyclerview/api/current.ignore
@@ -1,3 +1,5 @@
// Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
- Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_current.txt b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_current.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_current.ignore b/recyclerview/recyclerview/api/restricted_current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/restricted_current.ignore
+++ b/recyclerview/recyclerview/api/restricted_current.ignore
@@ -1,3 +1,5 @@
// Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
- Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_current.txt
+++ b/recyclerview/recyclerview/api/restricted_current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 8386fcf..771487a 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -156,7 +156,11 @@
*
* @param context Current context, will be used to access resources.
*/
- public LinearLayoutManager(@NonNull Context context) {
+ public LinearLayoutManager(
+ // Suppressed because fixing it requires a source-incompatible change to a very
+ // commonly used constructor, for no benefit: the context parameter is unused
+ @SuppressLint("UnknownNullness") Context context
+ ) {
this(context, RecyclerView.DEFAULT_ORIENTATION, false);
}
@@ -166,8 +170,13 @@
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
- public LinearLayoutManager(@NonNull Context context, @RecyclerView.Orientation int orientation,
- boolean reverseLayout) {
+ public LinearLayoutManager(
+ // Suppressed because fixing it requires a source-incompatible change to a very
+ // commonly used constructor, for no benefit: the context parameter is unused
+ @SuppressLint("UnknownNullness") Context context,
+ @RecyclerView.Orientation int orientation,
+ boolean reverseLayout
+ ) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index e43619f..c500f7e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -36,8 +36,10 @@
import androidx.room.solver.query.result.QueryResultBinder
import androidx.room.solver.shortcut.binder.CallableDeleteOrUpdateMethodBinder.Companion.createDeleteOrUpdateBinder
import androidx.room.solver.shortcut.binder.CallableInsertMethodBinder.Companion.createInsertBinder
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.transaction.binder.CoroutineTransactionMethodBinder
import androidx.room.solver.transaction.binder.InstantTransactionMethodBinder
import androidx.room.solver.transaction.binder.TransactionMethodBinder
@@ -91,6 +93,11 @@
abstract fun findDeleteOrUpdateMethodBinder(returnType: XType): DeleteOrUpdateMethodBinder
+ abstract fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder
+
abstract fun findTransactionMethodBinder(
callType: TransactionMethod.CallType
): TransactionMethodBinder
@@ -176,6 +183,11 @@
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
context.typeAdapterStore.findDeleteOrUpdateMethodBinder(returnType)
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = context.typeAdapterStore.findUpsertMethodBinder(returnType, params)
+
override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
InstantTransactionMethodBinder(
TransactionMethodAdapter(executableElement.jvmName, callType)
@@ -255,6 +267,23 @@
)
}
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = createUpsertBinder(
+ typeArg = returnType,
+ adapter = context.typeAdapterStore.findUpsertAdapter(returnType, params)
+ ) { callableImpl, dbField ->
+ addStatement(
+ "return $T.execute($N, $L, $L, $N)",
+ RoomCoroutinesTypeNames.COROUTINES_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl,
+ continuationParam.name
+ )
+ }
+
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
createDeleteOrUpdateBinder(
typeArg = returnType,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 2f1aff8..879a7b3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -208,6 +208,11 @@
params: List<ShortcutQueryParameter>
) = delegate.findInsertMethodBinder(returnType, params)
+ fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = delegate.findUpsertMethodBinder(returnType, params)
+
fun findDeleteOrUpdateMethodBinder(returnType: XType) =
delegate.findDeleteOrUpdateMethodBinder(returnType)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 1e1ac0d..cc06a34 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -81,16 +81,22 @@
import androidx.room.solver.query.result.SingleNamedColumnRowAdapter
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.shortcut.binderprovider.DeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.UpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.InstantUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
import androidx.room.solver.shortcut.result.InsertMethodAdapter
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
import androidx.room.solver.types.BoxedBooleanToBoxedIntConverter
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.ByteArrayColumnTypeAdapter
@@ -233,6 +239,13 @@
add(InstantDeleteOrUpdateMethodBinderProvider(context))
}
+ val upsertBinderProviders: List<UpsertMethodBinderProvider> =
+ mutableListOf<UpsertMethodBinderProvider>().apply {
+ addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
+ add(GuavaListenableFutureUpsertMethodBinderProvider(context))
+ add(InstantUpsertMethodBinderProvider(context))
+ }
+
/**
* Searches 1 way to bind a value into a statement.
*/
@@ -391,6 +404,15 @@
}.provide(typeMirror, params)
}
+ fun findUpsertMethodBinder(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return upsertBinderProviders.first {
+ it.matches(typeMirror)
+ }.provide(typeMirror, params)
+ }
+
fun findQueryResultBinder(
typeMirror: XType,
query: ParsedQuery,
@@ -432,6 +454,15 @@
return InsertMethodAdapter.create(typeMirror, params)
}
+ @Suppress("UNUSED_PARAMETER") // param will be used in a future change
+ fun findUpsertAdapter(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodAdapter? {
+ // TODO: change for UpsertMethodAdapter when bind has been created
+ return null
+ }
+
fun findQueryResultAdapter(
typeMirror: XType,
query: ParsedQuery,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
new file mode 100644
index 0000000..37aa163
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.room.solver.shortcut.binder
+
+import androidx.room.ext.CallableTypeSpecBuilder
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder for deferred upsert methods.
+ *
+ * This binder will create a Callable implementation that delegates to the
+ * [UpsertMethodAdapter]. Usage of the Callable impl is then delegate to the [addStmntBlock]
+ * function.
+ */
+class CallableUpsertMethodBinder(
+ val typeArg: XType,
+ val addStmntBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit,
+ adapter: UpsertMethodAdapter?
+) : UpsertMethodBinder(adapter) {
+
+ companion object {
+ fun createUpsertBinder(
+ typeArg: XType,
+ adapter: UpsertMethodAdapter?,
+ addCodeBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit
+ ) = CallableUpsertMethodBinder(typeArg, addCodeBlock, adapter)
+ }
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ val adapterScope = scope.fork()
+ val callableImpl = CallableTypeSpecBuilder(typeArg.typeName) {
+ // TODO add the createMethodBody in UpsertMethodAdapter
+ addCode(adapterScope.generate())
+ }.build()
+
+ scope.builder().apply {
+ addStmntBlock(callableImpl, dbField)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
new file mode 100644
index 0000000..06860f7
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 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.room.solver.shortcut.binder
+
+import androidx.room.ext.N
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder that knows how to write instant (blocking) upsert methods.
+ */
+class InstantUpsertMethodBinder(adapter: UpsertMethodAdapter?) : UpsertMethodBinder(adapter) {
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ scope.builder().apply {
+ addStatement("$N.assertNotSuspendingTransaction()", DaoWriter.dbField)
+ }
+ // TODO: createUpsertionMethodBody
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
index ff8c34f..9ec0168 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
@@ -21,8 +21,33 @@
import androidx.room.vo.ShortcutQueryParameter
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.TypeSpec
-
+/**
+ * Connects the upsert method, the database and the [UpsertMethodAdapter].
+ */
abstract class UpsertMethodBinder(val adapter: UpsertMethodAdapter?) {
+
+ /**
+ * Received the upsert method parameters, the upsertion adapters and generations the code that
+ * runs the upsert and returns the result.
+ *
+ * For example, for the DAO method:
+ * ```
+ * @Upsert
+ * fun addPublishers(vararg publishers: Publisher): List<Long>
+ * ```
+ * The following code will be generated:
+ *
+ * ```
+ * __db.beginTransaction();
+ * try {
+ * List<Long> _result = __upsertionAdapterOfPublisher.upsertAndReturnIdsList(publishers);
+ * __db.setTransactionSuccessful();
+ * return _result;
+ * } finally {
+ * __db.endTransaction();
+ * }
+ * ```
+ */
abstract fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..8f8cee0
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Guava ListenableFuture binders.
+ */
+class GuavaListenableFutureUpsertMethodBinderProvider(
+ private val context: Context
+) : UpsertMethodBinderProvider {
+
+ private val hasGuavaRoom by lazy {
+ context.processingEnv.findTypeElement(RoomGuavaTypeNames.GUAVA_ROOM) != null
+ }
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 &&
+ declared.rawType.typeName == GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ if (!hasGuavaRoom) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
+ }
+
+ val typeArg = declared.typeArguments.first()
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return createUpsertBinder(typeArg, adapter) { callableImpl, dbField ->
+ addStatement(
+ "return $T.createListenableFuture($N, $L, $L)",
+ RoomGuavaTypeNames.GUAVA_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..4a3ef91
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.solver.shortcut.binder.InstantUpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for instant (blocking) upsert method binder.
+ */
+class InstantUpsertMethodBinderProvider(private val context: Context) : UpsertMethodBinderProvider {
+
+ override fun matches(declared: XType) = true
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return InstantUpsertMethodBinder(
+ context.typeAdapterStore.findUpsertAdapter(declared, params)
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..df1911e
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.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.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XRawType
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.solver.RxType
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Rx Callable binders.
+ */
+open class RxCallableUpsertMethodBinderProvider internal constructor(
+ val context: Context,
+ private val rxType: RxType
+) : UpsertMethodBinderProvider {
+
+ /**
+ * [Single] and [Maybe] are generics but [Completable] is not so each implementation of this
+ * class needs to define how to extract the type argument.
+ */
+ open fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 && matchesRxType(declared)
+
+ private fun matchesRxType(declared: XType): Boolean {
+ return declared.rawType.typeName == rxType.className
+ }
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ val typeArg = extractTypeArg(declared)
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return CallableUpsertMethodBinder.createUpsertBinder(typeArg, adapter) { callableImpl, _ ->
+ addStatement("return $T.fromCallable($L)", rxType.className, callableImpl)
+ }
+ }
+
+ companion object {
+ fun getAll(context: Context) = listOf(
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX2_COMPLETABLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX3_COMPLETABLE)
+ )
+ }
+}
+
+private class RxCompletableUpsertMethodBinderProvider(
+ context: Context,
+ rxType: RxType
+) : RxCallableUpsertMethodBinderProvider(context, rxType) {
+
+ private val completableType: XRawType? by lazy {
+ context.processingEnv.findType(rxType.className)?.rawType
+ }
+
+ /**
+ * Since Completable is not a generic, the supported return type should be Void.
+ * Like this, the generated Callable.call method will return Void.
+ */
+ override fun extractTypeArg(declared: XType): XType =
+ context.COMMON_TYPES.VOID
+
+ override fun matches(declared: XType): Boolean = isCompletable(declared)
+
+ private fun isCompletable(declared: XType): Boolean {
+ if (completableType == null) {
+ return false
+ }
+ return declared.rawType.isAssignableFrom(completableType!!)
+ }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..0858f92
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for upsert method binders.
+ */
+interface UpsertMethodBinderProvider {
+
+ /**
+ * Check whether the [XType] can be handled by the [UpsertMethodBinder]
+ */
+ fun matches(declared: XType): Boolean
+
+ /**
+ * Provider of [UpsertMethodBinder], based on the [XType] and the list of parameters
+ */
+ fun provide(declared: XType, params: List<ShortcutQueryParameter>): UpsertMethodBinder
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 993cdd2..2fa5dbb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -56,8 +56,10 @@
import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.CompositeAdapter
import androidx.room.solver.types.CustomTypeConverterWrapper
@@ -793,6 +795,69 @@
}
@Test
+ fun testFindUpsertSingle() {
+ listOf(
+ Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
+ Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(single).isNotNull()
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(single.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertMaybe() {
+ listOf(
+ Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
+ Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(maybe.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertCompletable() {
+ listOf(
+ Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
+ Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(completable.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertListenableFuture() {
+ runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE)) {
+ invocation ->
+ val future = invocation.processingEnv
+ .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
+ assertThat(
+ GuavaListenableFutureUpsertMethodBinderProvider(invocation.context).matches(
+ future.type
+ )).isTrue()
+ }
+ }
+
+ @Test
fun testFindLiveData() {
runProcessorTest(
sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)
diff --git a/settings.gradle b/settings.gradle
index e5d3d62..c5e912a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -92,9 +92,9 @@
value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
- // Do not publish scan for androidx-platform-dev
- // publishAlways()
- // publishIfAuthenticated()
+ // Publish scan for androidx-main
+ publishAlways()
+ publishIfAuthenticated()
}
}
@@ -673,6 +673,7 @@
includeProject(":inspection:inspection-gradle-plugin", [BuildType.MAIN])
includeProject(":inspection:inspection-testing", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":interpolator:interpolator", [BuildType.MAIN])
+includeProject(":javascriptengine:javascriptengine", [BuildType.MAIN])
includeProject(":leanback:leanback", [BuildType.MAIN])
includeProject(":leanback:leanback-grid", [BuildType.MAIN])
includeProject(":leanback:leanback-paging", [BuildType.MAIN])
diff --git a/tracing/tracing-perfetto-common/api/current.txt b/tracing/tracing-perfetto-common/api/current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/current.txt
+++ b/tracing/tracing-perfetto-common/api/current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
+++ b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/restricted_current.txt b/tracing/tracing-perfetto-common/api/restricted_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/restricted_current.txt
+++ b/tracing/tracing-perfetto-common/api/restricted_current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
index 78696b3..d9fc71c 100644
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
@@ -62,7 +62,14 @@
" $pathExtra " +
"$targetPackage/$RECEIVER_CLASS_NAME"
val rawResponse = executeShellCommand(command)
- return parseResponse(rawResponse)
+
+ return try {
+ parseResponse(rawResponse)
+ } catch (e: IllegalArgumentException) {
+ val message = "Exception occurred while trying to parse a response." +
+ " Error: ${e.message}. Raw response: $rawResponse."
+ EnableTracingResponse(ResponseExitCodes.RESULT_CODE_ERROR_OTHER, null, message)
+ }
}
private fun parseResponse(rawResponse: String): EnableTracingResponse {
@@ -71,6 +78,10 @@
.firstOrNull { it.contains("Broadcast completed: result=") }
?: throw IllegalArgumentException("Cannot parse: $rawResponse")
+ if (line == "Broadcast completed: result=0") return EnableTracingResponse(
+ ResponseExitCodes.RESULT_CODE_CANCELLED, null, null
+ )
+
val matchResult =
Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?")
.matchEntire(line)
@@ -203,6 +214,15 @@
}
public object ResponseExitCodes {
+ /**
+ * Indicates that the broadcast resulted in `result=0`, which is an equivalent
+ * of [android.app.Activity.RESULT_CANCELED].
+ *
+ * This most likely means that the app does not expose a [PerfettoHandshake] compatible
+ * receiver.
+ */
+ public const val RESULT_CODE_CANCELLED: Int = 0
+
public const val RESULT_CODE_SUCCESS: Int = 1
public const val RESULT_CODE_ALREADY_ENABLED: Int = 2
@@ -228,6 +248,7 @@
@Retention(AnnotationRetention.SOURCE)
@IntDef(
+ ResponseExitCodes.RESULT_CODE_CANCELLED,
ResponseExitCodes.RESULT_CODE_SUCCESS,
ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED,
ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING,
@@ -239,7 +260,15 @@
public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
@EnableTracingResultCode public val exitCode: Int,
- public val requiredVersion: String,
+
+ /**
+ * This can be `null` iff we cannot communicate with the broadcast receiver of the target
+ * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
+ * from the receiver. In either case, tracing is unlikely to work under these circumstances,
+ * and more context on how to proceed can be found in [exitCode] or [message] properties.
+ */
+ public val requiredVersion: String?,
+
public val message: String?
)
}
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index ef5258f..6c20d7c 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -35,7 +35,7 @@
implementation(libs.kotlinStdlib)
implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 76fdfda..e67002e 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -40,7 +40,7 @@
implementation(project(":compose:material:material-ripple"))
implementation(project(":compose:ui:ui-util"))
implementation(project(":wear:compose:compose-foundation"))
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 1581111..bc7a4e8 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -32,7 +32,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.navigation:navigation-compose:2.4.0")
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
index 4d95812..a940dba 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
@@ -28,6 +28,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,6 +62,7 @@
* Test the algorithmic darkening on web content that doesn't support dark style.
*/
@Test
+ @Ignore("b/235864049") // Find a way to run with targetSdk T
public void testSimplifiedDarkMode_rendersDark() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 6808524..d67a87b 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -40,7 +40,7 @@
public WebSettingsCompatLightThemeTest() {
// targetSdkVersion to T, it is min version the algorithmic darkening works.
// TODO(http://b/214741472): Use VERSION_CODES.TIRAMISU once available.
- super(WebViewLightThemeTestActivity.class, VERSION_CODES.CUR_DEVELOPMENT);
+ super(WebViewLightThemeTestActivity.class, 33);
}
/**
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 1b5b5ac..a917e8e 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -25,6 +25,7 @@
import static org.hamcrest.Matchers.greaterThan;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -37,6 +38,7 @@
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -199,6 +201,30 @@
}
@Test
+ public void test_InitializationExceptionHandler_migrationFailures() {
+ mContext = mock(Context.class);
+ when(mContext.getApplicationContext()).thenReturn(mContext);
+ mWorkDatabase = WorkDatabase.create(mContext, mConfiguration.getTaskExecutor(), true);
+ when(mWorkManager.getWorkDatabase()).thenReturn(mWorkDatabase);
+ mRunnable = new ForceStopRunnable(mContext, mWorkManager);
+
+ InitializationExceptionHandler handler = mock(InitializationExceptionHandler.class);
+ Configuration configuration = new Configuration.Builder(mConfiguration)
+ .setInitializationExceptionHandler(handler)
+ .build();
+
+ when(mWorkManager.getConfiguration()).thenReturn(configuration);
+ // This is what WorkDatabasePathHelper uses under the hood to migrate the database.
+ when(mContext.getDatabasePath(anyString())).thenThrow(
+ new SQLiteException("Unable to migrate database"));
+
+ ForceStopRunnable runnable = spy(mRunnable);
+ doNothing().when(runnable).sleep(anyLong());
+ runnable.run();
+ verify(handler, times(1)).handleException(any(Throwable.class));
+ }
+
+ @Test
public void test_completeOnMultiProcessChecks() {
ForceStopRunnable runnable = spy(mRunnable);
doReturn(false).when(runnable).multiProcessChecks();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 805b8ee..cf9679b 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -38,6 +38,7 @@
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteTableLockedException;
import android.os.Build;
import android.text.TextUtils;
@@ -102,8 +103,29 @@
return;
}
while (true) {
- // Migrate the database to the no-backup directory if necessary.
- WorkDatabasePathHelper.migrateDatabase(mContext);
+
+ try {
+ // Migrate the database to the no-backup directory if necessary.
+ // Migrations are not retry-able. So if something unexpected were to happen
+ // here, the best we can do is to hand things off to the
+ // InitializationExceptionHandler.
+ WorkDatabasePathHelper.migrateDatabase(mContext);
+ } catch (SQLiteException sqLiteException) {
+ // This should typically never happen.
+ String message = "Unexpected SQLite exception during migrations";
+ Logger.get().error(TAG, message);
+ IllegalStateException exception =
+ new IllegalStateException(message, sqLiteException);
+ InitializationExceptionHandler exceptionHandler =
+ mWorkManager.getConfiguration().getInitializationExceptionHandler();
+ if (exceptionHandler != null) {
+ exceptionHandler.handleException(exception);
+ break;
+ } else {
+ throw exception;
+ }
+ }
+
// Clean invalid jobs attributed to WorkManager, and Workers that might have been
// interrupted because the application crashed (RUNNING state).
Logger.get().debug(TAG, "Performing cleanup operations.");
@@ -111,11 +133,11 @@
forceStopRunnable();
break;
} catch (SQLiteCantOpenDatabaseException
- | SQLiteDatabaseCorruptException
- | SQLiteDatabaseLockedException
- | SQLiteTableLockedException
- | SQLiteConstraintException
- | SQLiteAccessPermException exception) {
+ | SQLiteDatabaseCorruptException
+ | SQLiteDatabaseLockedException
+ | SQLiteTableLockedException
+ | SQLiteConstraintException
+ | SQLiteAccessPermException exception) {
mRetryCount++;
if (mRetryCount >= MAX_ATTEMPTS) {
// ForceStopRunnable is usually the first thing that accesses a database