Merge "Fix ImageCaptureTest failures in the extensions module" into androidx-main
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index 4deb295..a57afdd 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -40,6 +40,7 @@
import kotlin.test.assertTrue
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -181,6 +182,7 @@
validateStartup_fullyDrawn(delayMs = 100)
}
+ @Ignore // b/258335082
@LargeTest
@Test
fun startupInAppNav_immediate() {
@@ -188,6 +190,7 @@
validateStartup_fullyDrawn(delayMs = 0, useInAppNav = true)
}
+ @Ignore // b/258335082
@LargeTest
@Test
fun startupInAppNav_fullyDrawn() {
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 64ab132..a69dfd01 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
@@ -47,7 +47,7 @@
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
-private const val tracingPerfettoVersion = "1.0.0-alpha07" // TODO(224510255): get by 'reflection'
+private const val tracingPerfettoVersion = "1.0.0-alpha08" // TODO(224510255): get by 'reflection'
private const val minSupportedSdk = Build.VERSION_CODES.R // TODO(234351579): Support API < 30
@RunWith(Parameterized::class)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
index 9954170..d53c41d 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
@@ -86,34 +86,6 @@
}
@Test
- fun customComposeVersions() {
- val toml = """
- [versions]
- COMPOSE_V1 = "1.2.3"
- [groups]
- COMPOSE = { group = "androidx.compose.suffix", atomicGroupVersion = "versions.COMPOSE_V1" }
- """.trimIndent()
- val noCustomVersion = createLibraryVersionsService(toml)
- assertThat(
- noCustomVersion.libraryGroups["COMPOSE"]
- ).isEqualTo(
- LibraryGroup(
- group = "androidx.compose.suffix", atomicGroupVersion = Version("1.2.3")
- )
- )
- val customComposeVersion = createLibraryVersionsService(
- toml, composeCustomGroup = "not.androidx.compose", composeCustomVersion = "1.1.1"
- )
- assertThat(
- customComposeVersion.libraryGroups["COMPOSE"]
- ).isEqualTo(
- LibraryGroup(
- group = "not.androidx.compose.suffix", atomicGroupVersion = Version("1.1.1")
- )
- )
- }
-
- @Test
fun missingVersionReference() {
val service = createLibraryVersionsService(
"""
@@ -217,8 +189,6 @@
private fun createLibraryVersionsService(
tomlFile: String,
- composeCustomVersion: String? = null,
- composeCustomGroup: String? = null,
useMultiplatformGroupVersions: Boolean = false
): LibraryVersionsService {
val project = ProjectBuilder.builder().withProjectDir(tempDir.newFolder()).build()
@@ -228,16 +198,10 @@
spec.parameters.tomlFile = project.provider {
tomlFile
}
- spec.parameters.composeCustomVersion = project.provider {
- composeCustomVersion
- }
- spec.parameters.composeCustomGroup = project.provider {
- composeCustomGroup
- }
spec.parameters.useMultiplatformGroupVersions = project.provider {
useMultiplatformGroupVersions
}
}
return serviceProvider.get()
}
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 4099584..820c82b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -39,8 +39,6 @@
File(project.getSupportRootFolder(), "libraryversions.toml")
)
val content = project.providers.fileContents(toml)
- val composeCustomVersion = project.providers.environmentVariable("COMPOSE_CUSTOM_VERSION")
- val composeCustomGroup = project.providers.environmentVariable("COMPOSE_CUSTOM_GROUP")
val useMultiplatformVersions = project.provider {
Multiplatform.isKotlinNativeEnabled(project)
}
@@ -50,8 +48,6 @@
LibraryVersionsService::class.java
) { spec ->
spec.parameters.tomlFile = content.asText
- spec.parameters.composeCustomVersion = composeCustomVersion
- spec.parameters.composeCustomGroup = composeCustomGroup
spec.parameters.useMultiplatformGroupVersions = useMultiplatformVersions
}
LibraryGroups = serviceProvider.get().libraryGroups
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
index 426bc93..bb8852f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
@@ -30,8 +30,6 @@
abstract class LibraryVersionsService : BuildService<LibraryVersionsService.Parameters> {
interface Parameters : BuildServiceParameters {
var tomlFile: Provider<String>
- var composeCustomVersion: Provider<String>
- var composeCustomGroup: Provider<String>
var useMultiplatformGroupVersions: Provider<Boolean>
}
@@ -43,14 +41,7 @@
val versions = parsedTomlFile.getTable("versions")
?: throw GradleException("Library versions toml file is missing [versions] table")
versions.keySet().associateWith { versionName ->
- val versionValue =
- if (versionName.startsWith("COMPOSE") &&
- parameters.composeCustomVersion.isPresent
- ) {
- parameters.composeCustomVersion.get()
- } else {
- versions.getString(versionName)!!
- }
+ val versionValue = versions.getString(versionName)!!
Version.parseOrNull(versionValue)
?: throw GradleException(
"$versionName does not match expected format - $versionValue"
@@ -83,11 +74,7 @@
groups.keySet().associateWith { name ->
val groupDefinition = groups.getTable(name)!!
val groupName = groupDefinition.getString("group")!!
- val finalGroupName = if (name.startsWith("COMPOSE") &&
- parameters.composeCustomGroup.isPresent
- ) {
- groupName.replace("androidx.compose", parameters.composeCustomGroup.get())
- } else groupName
+ val finalGroupName = groupName
val atomicGroupVersion = readGroupVersion(
groupDefinition = groupDefinition,
@@ -116,4 +103,4 @@
private const val VersionReferencePrefix = "versions."
private const val AtomicGroupVersion = "atomicGroupVersion"
-private const val MultiplatformGroupVersion = "multiplatformGroupVersion"
\ No newline at end of file
+private const val MultiplatformGroupVersion = "multiplatformGroupVersion"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 2f49b0ac..6d7dade 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -21,6 +21,7 @@
import android.annotation.SuppressLint
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraMetadata
+import android.util.Size
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraPipe
@@ -128,6 +129,11 @@
}
}
+ override fun getSupportedResolutions(format: Int): List<Size> {
+ Log.warn { "TODO: getSupportedResolutions are not yet supported." }
+ return emptyList()
+ }
+
override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
override fun getCameraQuirks(): Quirks {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 0aafb22..6348136 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -18,6 +18,7 @@
import android.hardware.camera2.CameraCharacteristics
import android.os.Build
+import android.util.Size
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.integration.adapter.CameraControlStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
@@ -188,6 +189,10 @@
override fun getTimebase(): Timebase {
throw NotImplementedError("Not used in testing")
}
+
+ override fun getSupportedResolutions(format: Int): MutableList<Size> {
+ throw NotImplementedError("Not used in testing")
+ }
}
Camera2CameraInfo.from(wrongCameraInfo)
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
index b6949e6..6d48e3e7 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
@@ -16,12 +16,11 @@
package androidx.camera.camera2.internal
-import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
import android.media.CamcorderProfile
-import android.os.Build
import android.util.Size
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat
import androidx.camera.core.CameraSelector
import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.testing.CameraUtil
@@ -155,16 +154,11 @@
}
private fun getVideoSupportedResolutions(): Array<Size> {
- val map = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
-
- // Before Android 23, use {@link SurfaceTexture} will finally mapped to 0x22 in
- // StreamConfigurationMap to retrieve the output sizes information.
- return if (Build.VERSION.SDK_INT < 23) {
- map.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
- } else {
- map.getOutputSizes(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
- ?: emptyArray()
- }
+ val mapCompat = StreamConfigurationMapCompat.toStreamConfigurationMapCompat(
+ cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
+ )
+ return mapCompat.getOutputSizes(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+ ?: emptyArray()
}
private fun CamcorderProfile.size() = Size(videoFrameWidth, videoFrameHeight)
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 6d5f28a..4176afb 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -22,10 +22,14 @@
import static androidx.camera.camera2.internal.ZslUtil.isCapabilitySupported;
+import static java.util.Objects.requireNonNull;
+
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Build;
import android.util.Pair;
+import android.util.Size;
import android.view.Surface;
import androidx.annotation.GuardedBy;
@@ -36,6 +40,7 @@
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
@@ -61,6 +66,8 @@
import androidx.lifecycle.Observer;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -392,6 +399,17 @@
}
}
+ @NonNull
+ @Override
+ public List<Size> getSupportedResolutions(int format) {
+ StreamConfigurationMap map = requireNonNull(mCameraCharacteristicsCompat.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+ Size[] size = mapCompat.getOutputSizes(format);
+ return size != null ? Arrays.asList(size) : Collections.emptyList();
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
index 04688d1..66d79fe3 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
@@ -21,7 +21,6 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.params.StreamConfigurationMap;
-import android.os.Build;
import android.util.Size;
import android.view.Surface;
@@ -29,6 +28,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.SupportedRepeatingSurfaceSize;
import androidx.camera.core.Logger;
import androidx.camera.core.UseCase;
@@ -166,13 +166,9 @@
return new Size(0, 0);
}
- if (Build.VERSION.SDK_INT < 23) {
- // ImageFormat.PRIVATE is only public after Android level 23. Therefore, using
- // SurfaceTexture.class to get the supported output sizes before Android level 23.
- outputSizes = map.getOutputSizes(SurfaceTexture.class);
- } else {
- outputSizes = map.getOutputSizes(ImageFormat.PRIVATE);
- }
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+ outputSizes = mapCompat.getOutputSizes(ImageFormat.PRIVATE);
if (outputSizes == null) {
Logger.e(TAG, "Can not get output size list.");
return new Size(0, 0);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
index 589530a..f901f3b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
@@ -24,7 +24,6 @@
import android.graphics.ImageFormat;
import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Build;
@@ -36,13 +35,13 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.Logger;
import androidx.camera.core.ResolutionSelector;
-import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.SizeCoordinate;
import androidx.camera.core.impl.SurfaceConfig;
@@ -294,8 +293,6 @@
@NonNull
private Size[] doGetOutputSizesByFormat(int imageFormat) {
- Size[] outputSizes;
-
StreamConfigurationMap map =
mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
@@ -303,18 +300,9 @@
throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
}
- if (Build.VERSION.SDK_INT < 23
- && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
- // This is a little tricky that 0x22 that is internal defined in
- // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
- // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
- // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
- // retrieve the output sizes information.
- outputSizes = map.getOutputSizes(SurfaceTexture.class);
- } else {
- outputSizes = map.getOutputSizes(imageFormat);
- }
-
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+ Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
if (outputSizes == null) {
throw new IllegalArgumentException(
"Can not get supported output size for the format: " + imageFormat);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 25bf0f8..40e04f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -36,12 +36,10 @@
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
-import android.os.Build;
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
@@ -54,6 +52,7 @@
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
@@ -63,7 +62,6 @@
import androidx.camera.core.Logger;
import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.impl.AttachedSurfaceInfo;
-import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
@@ -693,8 +691,6 @@
@NonNull
private Size[] doGetAllOutputSizesByFormat(int imageFormat) {
- Size[] outputSizes;
-
StreamConfigurationMap map =
mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
@@ -702,18 +698,9 @@
throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
}
- if (Build.VERSION.SDK_INT < 23
- && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
- // This is a little tricky that 0x22 that is internal defined in
- // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
- // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
- // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
- // retrieve the output sizes information.
- outputSizes = map.getOutputSizes(SurfaceTexture.class);
- } else {
- outputSizes = map.getOutputSizes(imageFormat);
- }
-
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+ Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
if (outputSizes == null) {
throw new IllegalArgumentException(
"Can not get supported output size for the format: " + imageFormat);
@@ -815,7 +802,10 @@
throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
}
- Size[] videoSizeArr = map.getOutputSizes(MediaRecorder.class);
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+
+ Size[] videoSizeArr = mapCompat.getOutputSizes(MediaRecorder.class);
if (videoSizeArr == null) {
return RESOLUTION_480P;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java
new file mode 100644
index 0000000..e8d40e9
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java
@@ -0,0 +1,95 @@
+/*
+ * 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.camera2.internal.compat;
+
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Helper for accessing features in {@link StreamConfigurationMap} in a backwards compatible
+ * fashion.
+ */
+@RequiresApi(21)
+public class StreamConfigurationMapCompat {
+
+ private final StreamConfigurationMapCompatImpl mImpl;
+
+ private StreamConfigurationMapCompat(@NonNull StreamConfigurationMap map) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ mImpl = new StreamConfigurationMapCompatApi23Impl(map);
+ } else {
+ mImpl = new StreamConfigurationMapCompatBaseImpl(map);
+ }
+ }
+
+ /**
+ * Provides a backward-compatible wrapper for {@link StreamConfigurationMap}.
+ *
+ * @param map {@link StreamConfigurationMap} class to wrap
+ * @return wrapped class
+ */
+ @NonNull
+ public static StreamConfigurationMapCompat toStreamConfigurationMapCompat(
+ @NonNull StreamConfigurationMap map) {
+ return new StreamConfigurationMapCompat(map);
+ }
+
+ /**
+ * Get a list of sizes compatible with the requested image {@code format}.
+ *
+ * @param format an image format from {@link ImageFormat} or {@link PixelFormat}
+ * @return an array of supported sizes, or {@code null} if the {@code format} is not a
+ * supported output
+ *
+ * @see ImageFormat
+ * @see PixelFormat
+ */
+ @Nullable
+ public Size[] getOutputSizes(int format) {
+ return mImpl.getOutputSizes(format);
+ }
+
+ /**
+ * Get a list of sizes compatible with {@code klass} to use as an output.
+ *
+ * @param klass a non-{@code null} {@link Class} object reference
+ * @return an array of supported sizes for {@link ImageFormat#PRIVATE} format,
+ * or {@code null} iff the {@code klass} is not a supported output.
+ *
+ * @throws NullPointerException if {@code klass} was {@code null}
+ */
+ @Nullable
+ public <T> Size[] getOutputSizes(@NonNull Class<T> klass) {
+ return mImpl.getOutputSizes(klass);
+ }
+
+ interface StreamConfigurationMapCompatImpl {
+
+ @Nullable
+ Size[] getOutputSizes(int format);
+
+ @Nullable
+ <T> Size[] getOutputSizes(@NonNull Class<T> klass);
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
new file mode 100644
index 0000000..23716af
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
@@ -0,0 +1,38 @@
+/*
+ * 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.camera2.internal.compat;
+
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class StreamConfigurationMapCompatApi23Impl extends StreamConfigurationMapCompatBaseImpl {
+
+ StreamConfigurationMapCompatApi23Impl(@NonNull StreamConfigurationMap map) {
+ super(map);
+ }
+
+ @Nullable
+ @Override
+ public Size[] getOutputSizes(int format) {
+ return mStreamConfigurationMap.getOutputSizes(format);
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
new file mode 100644
index 0000000..5b797a1
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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.camera2.internal.compat;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.ImageFormatConstants;
+
+@RequiresApi(21)
+class StreamConfigurationMapCompatBaseImpl
+ implements StreamConfigurationMapCompat.StreamConfigurationMapCompatImpl {
+
+ final StreamConfigurationMap mStreamConfigurationMap;
+
+ StreamConfigurationMapCompatBaseImpl(@NonNull StreamConfigurationMap map) {
+ mStreamConfigurationMap = map;
+ }
+
+ @Nullable
+ @Override
+ public Size[] getOutputSizes(int format) {
+ Size[] sizes;
+ if (format == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+ // This is a little tricky that 0x22 that is internal defined in
+ // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+ // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+ // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+ // retrieve the output sizes information.
+ sizes = mStreamConfigurationMap.getOutputSizes(SurfaceTexture.class);
+ } else {
+ sizes = mStreamConfigurationMap.getOutputSizes(format);
+ }
+ return sizes;
+ }
+
+ @Nullable
+ @Override
+ public <T> Size[] getOutputSizes(@NonNull Class<T> klass) {
+ return mStreamConfigurationMap.getOutputSizes(klass);
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
index d058add..fb2d013 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
@@ -16,16 +16,15 @@
package androidx.camera.camera2.internal.compat.quirk;
-import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
-import android.os.Build;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.CamcorderProfileResolutionValidator;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.ImageFormatConstants;
@@ -69,21 +68,17 @@
public CamcorderProfileResolutionQuirk(
@NonNull CameraCharacteristicsCompat characteristicsCompat) {
+ Size[] sizes = null;
StreamConfigurationMap map =
characteristicsCompat.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- if (map == null) {
+ if (map != null) {
+ StreamConfigurationMapCompat mapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+ sizes = mapCompat.getOutputSizes(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+ } else {
Logger.e(TAG, "StreamConfigurationMap is null");
}
- Size[] sizes;
- // Before Android 23, use {@link SurfaceTexture} will finally mapped to 0x22 in
- // StreamConfigurationMap to retrieve the output sizes information.
- if (Build.VERSION.SDK_INT < 23) {
- sizes = map != null ? map.getOutputSizes(SurfaceTexture.class) : null;
- } else {
- sizes = map != null ? map.getOutputSizes(
- ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) : null;
- }
-
mSupportedResolutions = sizes != null ? Arrays.asList(sizes.clone())
: Collections.emptyList();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 0062a4b..ae0f83b 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -32,6 +32,7 @@
import android.hardware.camera2.CameraManager;
import android.os.Build;
import android.util.Pair;
+import android.util.Size;
import android.view.Surface;
import androidx.annotation.NonNull;
@@ -48,6 +49,7 @@
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.internal.ImmutableZoomState;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -61,6 +63,7 @@
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowCameraCharacteristics;
import org.robolectric.shadows.ShadowCameraManager;
+import org.robolectric.shadows.StreamConfigurationMapBuilder;
import org.robolectric.util.ReflectionHelpers;
import java.util.Arrays;
@@ -543,6 +546,22 @@
assertThat(cameraInfo.isZslSupported()).isTrue();
}
+ @Test
+ public void canReturnSupportedResolutions() throws CameraAccessExceptionCompat {
+ init(/* hasReprocessingCapabilities = */ true);
+
+ Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
+ mCameraManagerCompat);
+ List<Size> resolutions = cameraInfo.getSupportedResolutions(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+
+ assertThat(resolutions).containsExactly(
+ new Size(1920, 1080),
+ new Size(1280, 720),
+ new Size(640, 480)
+ );
+ }
+
private CameraManagerCompat initCameraManagerWithPhysicalIds(
List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
@@ -595,6 +614,18 @@
shadowCharacteristics0.set(
CameraCharacteristics.FLASH_INFO_AVAILABLE, CAMERA0_FLASH_INFO_BOOLEAN);
+ // Mock the supported resolutions
+ {
+ int formatPrivate = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+ StreamConfigurationMapBuilder streamMapBuilder =
+ StreamConfigurationMapBuilder.newBuilder()
+ .addOutputSize(formatPrivate, new Size(1920, 1080))
+ .addOutputSize(formatPrivate, new Size(1280, 720))
+ .addOutputSize(formatPrivate, new Size(640, 480));
+ shadowCharacteristics0.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ streamMapBuilder.build());
+ }
+
// Mock the request capability
if (hasReprocessingCapabilities) {
shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
@@ -628,6 +659,17 @@
shadowCharacteristics1.set(
CameraCharacteristics.FLASH_INFO_AVAILABLE, CAMERA1_FLASH_INFO_BOOLEAN);
+ // Mock the supported resolutions
+ {
+ int formatPrivate = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+ StreamConfigurationMapBuilder streamMapBuilder =
+ StreamConfigurationMapBuilder.newBuilder()
+ .addOutputSize(formatPrivate, new Size(1280, 720))
+ .addOutputSize(formatPrivate, new Size(640, 480));
+ shadowCharacteristics1.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ streamMapBuilder.build());
+ }
+
// Add the camera to the camera service
((ShadowCameraManager)
Shadow.extract(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt
new file mode 100644
index 0000000..e602ed3
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.camera2.internal.compat
+
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.ImageFormatConstants
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+/**
+ * Unit tests for [StreamConfigurationMapCompat].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamConfigurationMapCompatTest {
+
+ companion object {
+ private val SIZE_480P = Size(640, 480)
+ private val SIZE_720P = Size(1080, 720)
+ private val SIZE_1080P = Size(1920, 1080)
+ private const val FORMAT_PRIVATE =
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ }
+
+ private lateinit var streamConfigurationMapCompat: StreamConfigurationMapCompat
+ private val privateFormatOutputSizes = listOf(SIZE_1080P, SIZE_720P, SIZE_480P)
+
+ @Before
+ fun setUp() {
+ val builder = StreamConfigurationMapBuilder.newBuilder().apply {
+ privateFormatOutputSizes.forEach { size ->
+ addOutputSize(FORMAT_PRIVATE, size)
+ }
+ }
+ streamConfigurationMapCompat =
+ StreamConfigurationMapCompat.toStreamConfigurationMapCompat(builder.build())
+ }
+
+ @Test
+ fun getOutputSizes_withFormat_callGetOutputSizes() {
+ assertThat(
+ streamConfigurationMapCompat.getOutputSizes(FORMAT_PRIVATE)!!.toList()
+ ).containsExactlyElementsIn(privateFormatOutputSizes)
+ }
+
+ @Test
+ fun getOutputSizes_withClass_callGetOutputSizes() {
+ assertThat(
+ streamConfigurationMapCompat.getOutputSizes(SurfaceTexture::class.java)!!.toList()
+ ).containsExactlyElementsIn(privateFormatOutputSizes)
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 36096c8..755c3bf 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -16,6 +16,7 @@
package androidx.camera.core;
+import static androidx.camera.core.CameraEffect.PREVIEW;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
@@ -83,7 +84,6 @@
import androidx.camera.core.internal.ThreadConfig;
import androidx.camera.core.processing.Node;
import androidx.camera.core.processing.SettableSurface;
-import androidx.camera.core.processing.SurfaceEdge;
import androidx.camera.core.processing.SurfaceProcessorInternal;
import androidx.camera.core.processing.SurfaceProcessorNode;
import androidx.core.util.Consumer;
@@ -255,7 +255,7 @@
// Create nodes and edges.
mNode = new SurfaceProcessorNode(camera, mSurfaceProcessor);
SettableSurface cameraSurface = new SettableSurface(
- CameraEffect.PREVIEW,
+ PREVIEW,
resolution,
ImageFormat.PRIVATE,
new Matrix(),
@@ -264,9 +264,12 @@
getRelativeRotation(camera),
/*mirroring=*/isFrontCamera(camera),
this::notifyReset);
- SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
- SurfaceEdge outputEdge = mNode.transform(inputEdge);
- SettableSurface appSurface = outputEdge.getSurfaces().get(0);
+ SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(cameraSurface);
+ SurfaceProcessorNode.In nodeInput = SurfaceProcessorNode.In.of(
+ cameraSurface,
+ singletonList(outConfig));
+ SurfaceProcessorNode.Out nodeOutput = mNode.transform(nodeInput);
+ SettableSurface appSurface = requireNonNull(nodeOutput.get(outConfig));
// Send the app Surface to the app.
mSessionDeferrableSurface = cameraSurface;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 3bde336..4c87b7e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -82,6 +82,9 @@
/**
* Get the rotation degrees.
*
+ * TODO(b/259308680): hide this from the {@link SurfaceOutput} interface. This is only used
+ * for testing.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index 3f580a9..514debc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -16,6 +16,10 @@
package androidx.camera.core.impl;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.util.Size;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -24,6 +28,7 @@
import androidx.core.util.Preconditions;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -80,6 +85,15 @@
@NonNull
Timebase getTimebase();
+ /**
+ * Returns the supported resolutions of this camera based on the input image format.
+ *
+ * @param format an image format from {@link ImageFormat} or {@link PixelFormat}.
+ * @return a list of supported resolutions, or an empty list if the format is not supported.
+ */
+ @NonNull
+ List<Size> getSupportedResolutions(int format);
+
/** {@inheritDoc} */
@NonNull
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 081ccc1..87c1c95 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -129,6 +129,17 @@
}
/**
+ * Gets the size after cropping and rotating.
+ *
+ * @return rotated size
+ * @throws IllegalArgumentException if the rotation degrees is not a multiple of.
+ */
+ @NonNull
+ public static Size getRotatedSize(@NonNull Rect cropRect, int rotationDegrees) {
+ return rotateSize(rectToSize(cropRect), rotationDegrees);
+ }
+
+ /**
* Converts the degrees to within 360 degrees [0 - 359].
*/
public static int within360(int degrees) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index 8584670..bdb18cd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -21,6 +21,7 @@
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ImageReader;
@@ -63,6 +64,11 @@
* <--> {@link SurfaceOutput} --> {@link SurfaceProcessor}(surface consumer)
* </pre>
*
+ * TODO(b/241910577): rename this class to SurfaceEdge and make it compositing a
+ * DeferrableSurface instead of inheriting it. This is because in a stream sharing scenario, a
+ * downstream UseCase may reset and provide a different Surface, and a DeferrableSurface
+ * cannot be attached to a different Surface.
+ *
* <p>For the full workflow, please see {@code SettableSurfaceTest
* #linkBothProviderAndConsumer_surfaceAndResultsArePropagatedE2E}
*/
@@ -253,14 +259,14 @@
* <p>Do not provide the {@link SurfaceOutput} to external target if the
* {@link ListenableFuture} fails.
*
- * @param resolution resolution of input image buffer
- * @param cropRect crop rect of input image buffer
- * @param rotationDegrees expected rotation to the input image buffer
- * @param mirroring expected mirroring to the input image buffer
+ * @param inputSize resolution of input image buffer
+ * @param cropRect crop rect of input image buffer
+ * @param rotationDegrees expected rotation to the input image buffer
+ * @param mirroring expected mirroring to the input image buffer
*/
@MainThread
@NonNull
- public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size resolution,
+ public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size inputSize,
@NonNull Rect cropRect, int rotationDegrees, boolean mirroring) {
checkMainThread();
Preconditions.checkState(!mHasConsumer, "Consumer can only be linked once.");
@@ -274,7 +280,7 @@
return Futures.immediateFailedFuture(e);
}
SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
- getTargets(), getFormat(), getSize(), resolution, cropRect,
+ getTargets(), getFormat(), getSize(), inputSize, cropRect,
rotationDegrees, mirroring);
surfaceOutputImpl.getCloseFuture().addListener(this::decrementUseCount,
directExecutor());
@@ -331,6 +337,9 @@
/**
* The format of the {@link Surface}.
+ *
+ * TODO(259308680): hide the format. Currently the pipeline only supports
+ * {@link ImageFormat#PRIVATE}.
*/
public int getFormat() {
return getPrescribedStreamFormat();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
deleted file mode 100644
index 20bab64..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ /dev/null
@@ -1,49 +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.core.processing;
-
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-
-import com.google.auto.value.AutoValue;
-
-import java.util.List;
-
-/**
- * A data class represents a {@link Node} output that is based on {@link Surface}s.
- */
-@AutoValue
-public abstract class SurfaceEdge {
-
- /**
- * Gets output surfaces.
- *
- * TODO(b/234180399): consider switching to com.google.common.collect.ImmutableList.
- */
- @SuppressWarnings("AutoValueImmutableFields")
- @NonNull
- public abstract List<SettableSurface> getSurfaces();
-
- /**
- * Creates a {@link SurfaceEdge}.
- */
- @NonNull
- public static SurfaceEdge create(@NonNull List<SettableSurface> surfaces) {
- return new AutoValue_SurfaceEdge(surfaces);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 2918de6..5c51b4f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -34,6 +34,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
@@ -185,6 +186,16 @@
return mFormat;
}
+ @VisibleForTesting
+ public Rect getInputCropRect() {
+ return mInputCropRect;
+ }
+
+ @VisibleForTesting
+ public Size getInputSize() {
+ return mInputSize;
+ }
+
/**
* @inheritDoc
*/
@@ -193,6 +204,11 @@
return mRotationDegrees;
}
+ @VisibleForTesting
+ public boolean getMirroring() {
+ return mMirroring;
+ }
+
/**
* This method can be invoked by the processor implementation on any thread.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index de60c70..d00f151 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -17,35 +17,41 @@
package androidx.camera.core.processing;
import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
-import static androidx.camera.core.impl.utils.TransformUtils.is90or270;
-import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
import static androidx.camera.core.impl.utils.TransformUtils.sizeToRect;
import static androidx.camera.core.impl.utils.TransformUtils.sizeToRectF;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkArgument;
-
-import static java.util.Collections.singletonList;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.util.Size;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraEffect;
import androidx.camera.core.Logger;
import androidx.camera.core.ProcessingException;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Preconditions;
+import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* A {@link Node} implementation that wraps around the public {@link SurfaceProcessor} interface.
*
@@ -59,7 +65,8 @@
@RequiresApi(api = 21)
// TODO(b/233627260): remove once implemented.
@SuppressWarnings("UnusedVariable")
-public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
+public class SurfaceProcessorNode implements
+ Node<SurfaceProcessorNode.In, SurfaceProcessorNode.Out> {
private static final String TAG = "SurfaceProcessorNode";
@@ -69,9 +76,9 @@
final CameraInternal mCameraInternal;
// Guarded by main thread.
@Nullable
- private SurfaceEdge mOutputEdge;
+ private Out mOutput;
@Nullable
- private SurfaceEdge mInputEdge;
+ private In mInput;
/**
* Constructs the {@link SurfaceProcessorNode}.
@@ -91,51 +98,51 @@
@Override
@NonNull
@MainThread
- public SurfaceEdge transform(@NonNull SurfaceEdge inputEdge) {
+ public Out transform(@NonNull In input) {
Threads.checkMainThread();
- checkArgument(inputEdge.getSurfaces().size() == 1,
- "Multiple input stream not supported yet.");
- mInputEdge = inputEdge;
- SettableSurface inputSurface = inputEdge.getSurfaces().get(0);
- SettableSurface outputSurface = createOutputSurface(inputSurface);
- sendSurfacesToProcessorWhenReady(inputSurface, outputSurface);
- mOutputEdge = SurfaceEdge.create(singletonList(outputSurface));
- return mOutputEdge;
+ mInput = input;
+ mOutput = new Out();
+
+ SettableSurface inputSurface = input.getSurfaceEdge();
+ for (OutConfig config : input.getOutConfigs()) {
+ mOutput.put(config, transformSingleOutput(inputSurface, config));
+ }
+ sendSurfacesToProcessorWhenReady(inputSurface, mOutput);
+ return mOutput;
}
@NonNull
- private SettableSurface createOutputSurface(@NonNull SettableSurface inputSurface) {
+ private SettableSurface transformSingleOutput(@NonNull SettableSurface input,
+ @NonNull OutConfig outConfig) {
// TODO: Can be improved by only restarting part of the pipeline. e.g. only update the
// output Surface (between Effect/App), and still use the same input Surface (between
// Camera/Effect). It's just simpler for now.
- final Runnable onSurfaceInvalidated = inputSurface::invalidate;
+ final Runnable onSurfaceInvalidated = input::invalidate;
SettableSurface outputSurface;
- Size resolution = inputSurface.getSize();
- Rect cropRect = inputSurface.getCropRect();
- int rotationDegrees = inputSurface.getRotationDegrees();
- boolean mirroring = inputSurface.getMirroring();
-
- // Calculate rotated resolution and cropRect
- Size rotatedCroppedSize = is90or270(rotationDegrees)
- ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
- : rectToSize(cropRect);
+ Size inputSize = input.getSize();
+ Rect cropRect = outConfig.getCropRect();
+ int rotationDegrees = input.getRotationDegrees();
+ boolean mirroring = input.getMirroring();
// Calculate sensorToBufferTransform
android.graphics.Matrix sensorToBufferTransform =
- new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
- android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
- new RectF(cropRect), rotationDegrees, mirroring);
+ new android.graphics.Matrix(input.getSensorToBufferTransform());
+ android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(inputSize),
+ sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
sensorToBufferTransform.postConcat(imageTransform);
+ // TODO(b/259308680): Checks that the aspect ratio of the rotated crop rect matches the
+ // output size.
outputSurface = new SettableSurface(
- inputSurface.getTargets(),
- rotatedCroppedSize,
- inputSurface.getFormat(),
+ outConfig.getTargets(),
+ outConfig.getSize(),
+ input.getFormat(),
sensorToBufferTransform,
// The Surface transform cannot be carried over during buffer copy.
/*hasEmbeddedTransform=*/false,
- sizeToRect(rotatedCroppedSize),
+ // Crop rect is always the full size.
+ sizeToRect(outConfig.getSize()),
/*rotationDegrees=*/0,
/*mirroring=*/false,
onSurfaceInvalidated);
@@ -144,21 +151,33 @@
}
private void sendSurfacesToProcessorWhenReady(@NonNull SettableSurface input,
- @NonNull SettableSurface output) {
+ @NonNull Map<OutConfig, SettableSurface> outputs) {
SurfaceRequest surfaceRequest = input.createSurfaceRequest(mCameraInternal);
+ List<ListenableFuture<SurfaceOutput>> outputFutures = new ArrayList<>();
+ for (Map.Entry<OutConfig, SettableSurface> output : outputs.entrySet()) {
+ outputFutures.add(output.getValue().createSurfaceOutputFuture(
+ input.getSize(),
+ output.getKey().getCropRect(),
+ input.getRotationDegrees(),
+ input.getMirroring()));
+ }
setupRotationUpdates(
surfaceRequest,
- output,
+ outputs.values(),
input.getMirroring(),
input.getRotationDegrees());
- Futures.addCallback(output.createSurfaceOutputFuture(input.getSize(), input.getCropRect(),
- input.getRotationDegrees(), input.getMirroring()),
- new FutureCallback<SurfaceOutput>() {
+
+ ListenableFuture<List<SurfaceOutput>> outputListFuture = Futures.allAsList(outputFutures);
+ Futures.addCallback(outputListFuture,
+ new FutureCallback<List<SurfaceOutput>>() {
+
@Override
- public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
- Preconditions.checkNotNull(surfaceOutput);
+ public void onSuccess(@Nullable List<SurfaceOutput> outputs) {
+ Preconditions.checkNotNull(outputs);
try {
- mSurfaceProcessor.onOutputSurface(surfaceOutput);
+ for (SurfaceOutput output : outputs) {
+ mSurfaceProcessor.onOutputSurface(output);
+ }
mSurfaceProcessor.onInputSurface(surfaceRequest);
} catch (ProcessingException e) {
Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
@@ -167,9 +186,9 @@
@Override
public void onFailure(@NonNull Throwable t) {
- // Do not send surfaces to the processor if the downstream provider (e.g.
- // the app) fails to provide a Surface. Instead, notify the consumer that
- // the Surface will not be provided.
+ // Do not send surfaces to the processor if the downstream provider
+ // (e.g.the app) fails to provide a Surface. Instead, notify the
+ // consumer that the Surface will not be provided.
surfaceRequest.willNotProvideSurface();
}
}, mainThreadExecutor());
@@ -187,13 +206,13 @@
* input edge's rotation changes, we re-calculate the delta and notify the output edge.
*
* @param inputSurfaceRequest {@link SurfaceRequest} of the input edge.
- * @param outputSurface {@link SettableSurface} of the output edge.
+ * @param outputSurfaces {@link SettableSurface} of the output edge.
* @param mirrored whether the node mirrors the buffer.
* @param rotatedDegrees how much the node rotates the buffer.
*/
void setupRotationUpdates(
@NonNull SurfaceRequest inputSurfaceRequest,
- @NonNull SettableSurface outputSurface,
+ @NonNull Collection<SettableSurface> outputSurfaces,
boolean mirrored,
int rotatedDegrees) {
inputSurfaceRequest.setTransformationInfoListener(mainThreadExecutor(), info -> {
@@ -203,7 +222,10 @@
if (mirrored) {
rotationDegrees = -rotationDegrees;
}
- outputSurface.setRotationDegrees(within360(rotationDegrees));
+ rotationDegrees = within360(rotationDegrees);
+ for (SettableSurface output : outputSurfaces) {
+ output.setRotationDegrees(rotationDegrees);
+ }
});
}
@@ -214,12 +236,107 @@
public void release() {
mSurfaceProcessor.release();
mainThreadExecutor().execute(() -> {
- if (mOutputEdge != null) {
- for (SettableSurface surface : mOutputEdge.getSurfaces()) {
+ if (mOutput != null) {
+ for (SettableSurface surface : mOutput.values()) {
// The output DeferrableSurface will later be terminated by the processor.
surface.close();
}
}
});
}
+
+
+ /**
+ * The input of a {@link SurfaceProcessorNode}.
+ */
+ @AutoValue
+ public abstract static class In {
+
+ /**
+ * Gets the input stream.
+ *
+ * <p> {@link SurfaceProcessorNode} only supports a single input stream.
+ */
+ @NonNull
+ public abstract SettableSurface getSurfaceEdge();
+
+ /**
+ * Gets the config for generating output streams.
+ *
+ * <p>{@link SurfaceProcessorNode#transform} creates one {@link SettableSurface} per
+ * {@link OutConfig} in this list.
+ */
+ @SuppressWarnings("AutoValueImmutableFields")
+ @NonNull
+ public abstract List<OutConfig> getOutConfigs();
+
+ /**
+ * Creates a {@link In} instance.
+ */
+ @NonNull
+ public static In of(@NonNull SettableSurface edge, @NonNull List<OutConfig> configs) {
+ return new AutoValue_SurfaceProcessorNode_In(edge, configs);
+ }
+ }
+
+ /**
+ * The output of a {@link SurfaceProcessorNode}.
+ *
+ * <p>A map of {@link OutConfig} with their corresponding {@link SettableSurface}.
+ */
+ public static class Out extends HashMap<OutConfig, SettableSurface> {
+ }
+
+ /**
+ * Configuration of how to create an output stream from an input stream.
+ *
+ * <p>The value in this class will override the corresponding value in the
+ * {@link SettableSurface} class. The override is necessary when a single stream is shared
+ * to multiple output streams with different transformations. For example, if a single 4:3
+ * preview stream is shared to a 16:9 video stream, the video stream must override the crop
+ * rect.
+ */
+ @AutoValue
+ public abstract static class OutConfig {
+
+ /**
+ * The target {@link UseCase} of the output stream.
+ */
+ @CameraEffect.Targets
+ abstract int getTargets();
+
+ /**
+ * How the input should be cropped.
+ */
+ @NonNull
+ abstract Rect getCropRect();
+
+ /**
+ * The stream should scale to this size after cropping and rotating.
+ *
+ * <p>The input stream should be scaled to match this size after cropping and rotating
+ */
+ @NonNull
+ abstract Size getSize();
+
+ /**
+ * Creates an {@link OutConfig} instance from the input edge.
+ *
+ * <p>The result is an output edge with the input's transformation applied.
+ */
+ @NonNull
+ public static OutConfig of(@NonNull SettableSurface surface) {
+ return of(surface.getTargets(),
+ surface.getCropRect(),
+ getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()));
+ }
+
+ /**
+ * Creates an {@link OutConfig} instance with custom transformations.
+ */
+ @NonNull
+ public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size) {
+ return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size);
+ }
+ }
}
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 8a1c98f..3a2a8c0 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
@@ -24,6 +24,7 @@
import android.util.Rational
import android.util.Size
import android.view.Surface
+import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
@@ -324,9 +325,9 @@
shadowOf(getMainLooper()).idle()
// Assert: surfaceOutput received.
- assertThat(processor.surfaceOutput).isNotNull()
+ assertThat(processor.surfaceOutputs).hasSize(1)
assertThat(processor.isReleased).isFalse()
- assertThat(processor.isOutputSurfaceRequestedToClose).isFalse()
+ assertThat(processor.isOutputSurfaceRequestedToClose[PREVIEW]).isNull()
assertThat(processor.isInputSurfaceReleased).isFalse()
assertThat(appSurfaceReadyToRelease).isFalse()
// processor surface is provided to camera.
@@ -339,12 +340,12 @@
// Assert: processor and processor surface is released.
assertThat(processor.isReleased).isTrue()
- assertThat(processor.isOutputSurfaceRequestedToClose).isTrue()
+ assertThat(processor.isOutputSurfaceRequestedToClose[PREVIEW]).isTrue()
assertThat(processor.isInputSurfaceReleased).isTrue()
assertThat(appSurfaceReadyToRelease).isFalse()
// Act: close SurfaceOutput
- processor.surfaceOutput!!.close()
+ processor.surfaceOutputs[CameraEffect.PREVIEW]!!.close()
shadowOf(getMainLooper()).idle()
assertThat(appSurfaceReadyToRelease).isTrue()
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 43fac3e..9139c14 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -16,7 +16,7 @@
package androidx.camera.core.processing
-import android.graphics.PixelFormat
+import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.os.Build
@@ -24,12 +24,14 @@
import android.util.Size
import android.view.Surface
import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.utils.TransformUtils.is90or270
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
+import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
-import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
import com.google.common.truth.Truth.assertThat
@@ -51,76 +53,91 @@
class SurfaceProcessorNodeTest {
companion object {
- private const val TARGET = PREVIEW
- private const val FORMAT = PixelFormat.RGBA_8888
private const val ROTATION_DEGREES = 90
- private val SIZE = Size(640, 480)
- private val CROP_RECT = Rect(0, 0, 600, 400)
+ private const val MIRRORING = false
+ private val INPUT_SIZE = Size(640, 480)
+ private val PREVIEW_CROP_RECT = Rect(0, 0, 600, 400)
+ private val VIDEO_CROP_RECT = Rect(0, 0, 300, 200)
+ private val VIDEO_SIZE = Size(20, 30)
}
private lateinit var surfaceProcessorInternal: FakeSurfaceProcessorInternal
- private lateinit var appSurface: Surface
- private lateinit var appSurfaceTexture: SurfaceTexture
+ private lateinit var previewSurface: Surface
+ private lateinit var previewTexture: SurfaceTexture
+ private lateinit var previewOutConfig: OutConfig
+ private lateinit var videoSurface: Surface
+ private lateinit var videoTexture: SurfaceTexture
+ private lateinit var videoOutConfig: OutConfig
private lateinit var node: SurfaceProcessorNode
- private lateinit var inputEdge: SurfaceEdge
- private lateinit var outputSurfaceRequest: SurfaceRequest
- private var outputTransformInfo: TransformationInfo? = null
+ private lateinit var nodeInput: SurfaceProcessorNode.In
+ private lateinit var previewSurfaceRequest: SurfaceRequest
+ private lateinit var videoSurfaceRequest: SurfaceRequest
+ private lateinit var previewTransformInfo: TransformationInfo
+ private lateinit var videoTransformInfo: TransformationInfo
@Before
fun setup() {
- appSurfaceTexture = SurfaceTexture(0)
- appSurface = Surface(appSurfaceTexture)
+ previewTexture = SurfaceTexture(0)
+ previewSurface = Surface(previewTexture)
+ videoTexture = SurfaceTexture(0)
+ videoSurface = Surface(videoTexture)
surfaceProcessorInternal = FakeSurfaceProcessorInternal(mainThreadExecutor())
}
@After
fun tearDown() {
- appSurfaceTexture.release()
- appSurface.release()
+ previewTexture.release()
+ previewSurface.release()
+ videoTexture.release()
+ videoSurface.release()
surfaceProcessorInternal.release()
if (::node.isInitialized) {
node.release()
}
- if (::inputEdge.isInitialized) {
- inputEdge.surfaces.forEach { it.close() }
+ if (::nodeInput.isInitialized) {
+ nodeInput.surfaceEdge.close()
}
- if (::outputSurfaceRequest.isInitialized) {
- outputSurfaceRequest.deferrableSurface.close()
+ if (::previewSurfaceRequest.isInitialized) {
+ previewSurfaceRequest.deferrableSurface.close()
+ }
+ if (::videoSurfaceRequest.isInitialized) {
+ videoSurfaceRequest.deferrableSurface.close()
}
shadowOf(getMainLooper()).idle()
}
@Test
fun transformInput_applyCropRotateAndMirroring_outputIsCroppedAndRotated() {
- val cropRect = Rect(200, 100, 600, 400)
for (rotationDegrees in arrayOf(0, 90, 180, 270)) {
// Arrange.
createSurfaceProcessorNode()
createInputEdge(
- size = rectToSize(cropRect),
- cropRect = cropRect,
- rotationDegrees = rotationDegrees
+ previewRotationDegrees = rotationDegrees
)
// The result cropRect should have zero left and top.
val expectedCropRect = if (is90or270(rotationDegrees))
- Rect(0, 0, cropRect.height(), cropRect.width())
+ Rect(0, 0, PREVIEW_CROP_RECT.height(), PREVIEW_CROP_RECT.width())
else
- Rect(0, 0, cropRect.width(), cropRect.height())
+ Rect(0, 0, PREVIEW_CROP_RECT.width(), PREVIEW_CROP_RECT.height())
// Act.
- val outputEdge = node.transform(inputEdge)
+ val nodeOutput = node.transform(nodeInput)
// Assert: with transformation, the output size is cropped/rotated and the rotation
// degrees is reset.
- assertThat(outputEdge.surfaces).hasSize(1)
- val outputSurface = outputEdge.surfaces[0]
- assertThat(outputSurface.size).isEqualTo(rectToSize(expectedCropRect))
- assertThat(outputSurface.cropRect).isEqualTo(expectedCropRect)
- assertThat(outputSurface.rotationDegrees).isEqualTo(0)
+ val previewOutput = nodeOutput[previewOutConfig]!!
+ assertThat(previewOutput.size).isEqualTo(rectToSize(expectedCropRect))
+ assertThat(previewOutput.cropRect).isEqualTo(expectedCropRect)
+ assertThat(previewOutput.rotationDegrees).isEqualTo(0)
+ val videoOutput = nodeOutput[videoOutConfig]!!
+ assertThat(videoOutput.size).isEqualTo(VIDEO_SIZE)
+ assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
+ assertThat(videoOutput.rotationDegrees).isEqualTo(0)
// Clean up.
- inputEdge.surfaces[0].close()
+ nodeInput.surfaceEdge.close()
node.release()
+ shadowOf(getMainLooper()).idle()
}
}
@@ -132,15 +149,14 @@
createInputEdge(mirroring = mirroring)
// Act.
- val outputEdge = node.transform(inputEdge)
+ val nodeOutput = node.transform(nodeInput)
// Assert: the mirroring of output is always false.
- assertThat(outputEdge.surfaces).hasSize(1)
- val outputSurface = outputEdge.surfaces[0]
- assertThat(outputSurface.mirroring).isFalse()
+ assertThat(nodeOutput[previewOutConfig]!!.mirroring).isFalse()
+ assertThat(nodeOutput[videoOutConfig]!!.mirroring).isFalse()
// Clean up.
- inputEdge.surfaces[0].close()
+ nodeInput.surfaceEdge.close()
node.release()
}
}
@@ -149,30 +165,42 @@
fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
- createInputEdge(rotationDegrees = 90, cropRect = Rect(0, 0, 600, 400))
+ createInputEdge()
// Act.
- val outputEdge = node.transform(inputEdge)
- val outputSurface = outputEdge.surfaces[0]
- createOutputSurfaceRequestAndProvideSurface(outputSurface)
+ val nodeOutput = node.transform(nodeInput)
+ provideSurfaces(nodeOutput)
shadowOf(getMainLooper()).idle()
// Assert: surfaceOutput of SurfaceProcessor will consume the initial rotation degrees and
// output surface will receive 0 degrees.
- assertThat(surfaceProcessorInternal.surfaceOutput!!.rotationDegrees).isEqualTo(90)
- assertThat(outputTransformInfo!!.rotationDegrees).isEqualTo(0)
- assertThat(outputTransformInfo!!.cropRect).isEqualTo(Rect(0, 0, 400, 600))
+ val previewSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+ assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+ assertThat(previewSurfaceOutput.size).isEqualTo(Size(400, 600))
+ assertThat(previewSurfaceOutput.inputCropRect).isEqualTo(PREVIEW_CROP_RECT)
+ assertThat(previewTransformInfo.cropRect).isEqualTo(Rect(0, 0, 400, 600))
+ assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
+ assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+
+ val videoSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+ assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+ assertThat(videoSurfaceOutput.size).isEqualTo(VIDEO_SIZE)
+ assertThat(videoSurfaceOutput.inputCropRect).isEqualTo(VIDEO_CROP_RECT)
+ assertThat(videoTransformInfo.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(0)
+ assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
}
@Test
fun setRotationToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
- createInputEdge(rotationDegrees = 90)
- val inputSurface = inputEdge.surfaces[0]
- val outputEdge = node.transform(inputEdge)
- val outputSurface = outputEdge.surfaces[0]
- createOutputSurfaceRequestAndProvideSurface(outputSurface)
+ createInputEdge(previewRotationDegrees = 90)
+ val inputSurface = nodeInput.surfaceEdge
+ val nodeOutput = node.transform(nodeInput)
+ provideSurfaces(nodeOutput)
shadowOf(getMainLooper()).idle()
// Act.
@@ -181,8 +209,16 @@
// Assert: surfaceOutput of SurfaceProcessor will consume the initial rotation degrees and
// output surface will receive the remaining degrees.
- assertThat(surfaceProcessorInternal.surfaceOutput!!.rotationDegrees).isEqualTo(90)
- assertThat(outputTransformInfo!!.rotationDegrees).isEqualTo(180)
+ val previewSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+ assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
+ assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
+ assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+ val videoSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+ assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
+ assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
}
@Test
@@ -190,16 +226,16 @@
// Arrange.
createSurfaceProcessorNode()
createInputEdge()
- val inputSurface = inputEdge.surfaces[0]
- val outputEdge = node.transform(inputEdge)
- val outputSurface = outputEdge.surfaces[0]
+ val inputSurface = nodeInput.surfaceEdge
+ val nodeOutput = node.transform(nodeInput)
// Act.
- outputSurface.setProvider(Futures.immediateFuture(appSurface))
+ provideSurfaces(nodeOutput)
shadowOf(getMainLooper()).idle()
// Assert: processor receives app Surface. CameraX receives processor Surface.
- assertThat(surfaceProcessorInternal.outputSurface).isEqualTo(appSurface)
+ assertThat(surfaceProcessorInternal.outputSurfaces[PREVIEW]).isEqualTo(previewSurface)
+ assertThat(surfaceProcessorInternal.outputSurfaces[VIDEO_CAPTURE]).isEqualTo(videoSurface)
assertThat(inputSurface.surface.get()).isEqualTo(surfaceProcessorInternal.inputSurface)
}
@@ -208,8 +244,8 @@
// Arrange.
createSurfaceProcessorNode()
createInputEdge()
- val outputSurface = node.transform(inputEdge).surfaces[0]
- outputSurface.setProvider(Futures.immediateFuture(appSurface))
+ val nodeOutput = node.transform(nodeInput)
+ provideSurfaces(nodeOutput)
shadowOf(getMainLooper()).idle()
// Act: release the node.
@@ -218,30 +254,40 @@
// Assert: processor is released and has requested processor to close the SurfaceOutput
assertThat(surfaceProcessorInternal.isReleased).isTrue()
- assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose).isTrue()
+ assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose[PREVIEW]).isTrue()
+ assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isTrue()
}
private fun createInputEdge(
- target: Int = TARGET,
- size: Size = SIZE,
- format: Int = FORMAT,
+ previewTarget: Int = PREVIEW,
+ previewSize: Size = INPUT_SIZE,
+ format: Int = ImageFormat.PRIVATE,
sensorToBufferTransform: android.graphics.Matrix = android.graphics.Matrix(),
hasEmbeddedTransform: Boolean = true,
- cropRect: Rect = CROP_RECT,
- rotationDegrees: Int = ROTATION_DEGREES,
- mirroring: Boolean = false
+ previewCropRect: Rect = PREVIEW_CROP_RECT,
+ previewRotationDegrees: Int = ROTATION_DEGREES,
+ mirroring: Boolean = MIRRORING,
) {
val surface = SettableSurface(
- target,
- size,
+ previewTarget,
+ previewSize,
format,
sensorToBufferTransform,
hasEmbeddedTransform,
- cropRect,
- rotationDegrees,
+ previewCropRect,
+ previewRotationDegrees,
mirroring
) {}
- inputEdge = SurfaceEdge.create(listOf(surface))
+ videoOutConfig = OutConfig.of(
+ VIDEO_CAPTURE,
+ VIDEO_CROP_RECT,
+ VIDEO_SIZE
+ )
+ previewOutConfig = OutConfig.of(surface)
+ nodeInput = SurfaceProcessorNode.In.of(
+ surface,
+ listOf(previewOutConfig, videoOutConfig)
+ )
}
private fun createSurfaceProcessorNode() {
@@ -251,15 +297,20 @@
)
}
- private fun createOutputSurfaceRequestAndProvideSurface(
- settableSurface: SettableSurface,
- surface: Surface = appSurface
- ) {
- outputSurfaceRequest = settableSurface.createSurfaceRequest(FakeCamera()).apply {
- setTransformationInfoListener(mainThreadExecutor()) {
- outputTransformInfo = it
+ private fun provideSurfaces(nodeOutput: SurfaceProcessorNode.Out) {
+ previewSurfaceRequest =
+ nodeOutput[previewOutConfig]!!.createSurfaceRequest(FakeCamera()).apply {
+ setTransformationInfoListener(mainThreadExecutor()) {
+ previewTransformInfo = it
+ }
+ provideSurface(previewSurface, mainThreadExecutor()) { previewSurface.release() }
}
- provideSurface(surface, mainThreadExecutor()) { surface.release() }
- }
+ videoSurfaceRequest =
+ nodeOutput[videoOutConfig]!!.createSurfaceRequest(FakeCamera()).apply {
+ setTransformationInfoListener(mainThreadExecutor()) {
+ videoTransformInfo = it
+ }
+ provideSurface(videoSurface, mainThreadExecutor()) { videoSurface.release() }
+ }
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index ecf59053..361de26 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -18,6 +18,7 @@
import android.util.Range;
import android.util.Rational;
+import android.util.Size;
import android.view.Surface;
import androidx.annotation.NonNull;
@@ -43,7 +44,10 @@
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -59,6 +63,7 @@
private final int mLensFacing;
private final MutableLiveData<Integer> mTorchState = new MutableLiveData<>(TorchState.OFF);
private final MutableLiveData<ZoomState> mZoomLiveData;
+ private final Map<Integer, List<Size>> mSupportedResolutionMap = new HashMap<>();
private MutableLiveData<CameraState> mCameraStateLiveData;
private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
@@ -180,6 +185,13 @@
return mTimebase;
}
+ @NonNull
+ @Override
+ public List<Size> getSupportedResolutions(int format) {
+ List<Size> resolutions = mSupportedResolutionMap.get(format);
+ return resolutions != null ? resolutions : Collections.emptyList();
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
@@ -236,6 +248,11 @@
mTimebase = timebase;
}
+ /** Set the supported resolutions for testing */
+ public void setSupportedResolutions(int format, @NonNull List<Size> resolutions) {
+ mSupportedResolutionMap.put(format, resolutions);
+ }
+
/** Set the isPrivateReprocessingSupported flag for testing */
public void setPrivateReprocessingSupported(boolean supported) {
mIsPrivateReprocessingSupported = supported;
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
index b8d6e160..1ce5970 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
@@ -28,6 +28,8 @@
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.DeferrableSurface;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -44,12 +46,12 @@
@Nullable
private SurfaceRequest mSurfaceRequest;
- @Nullable
- private SurfaceOutput mSurfaceOutput;
+ @NonNull
+ private final Map<Integer, SurfaceOutput> mSurfaceOutputs = new HashMap<>();
boolean mIsInputSurfaceReleased;
- boolean mIsOutputSurfaceRequestedToClose;
+ private final Map<Integer, Boolean> mIsOutputSurfaceRequestedToClose = new HashMap<>();
- Surface mOutputSurface;
+ private final Map<Integer, Surface> mOutputSurfaces = new HashMap<>();
/**
* Creates a {@link SurfaceProcessor} that closes the {@link SurfaceOutput} automatically.
@@ -61,7 +63,7 @@
/**
* @param autoCloseSurfaceOutput if true, automatically close the {@link SurfaceOutput} once
* the close request is received. Otherwise, the test needs to
- * get {@link #getSurfaceOutput()} and call
+ * get {@link #getSurfaceOutputs()} and call
* {@link SurfaceOutput#close()} to avoid the "Completer GCed"
* error in {@link DeferrableSurface}.
*/
@@ -70,7 +72,6 @@
mInputSurface = new Surface(mSurfaceTexture);
mExecutor = executor;
mIsInputSurfaceReleased = false;
- mIsOutputSurfaceRequestedToClose = false;
mAutoCloseSurfaceOutput = autoCloseSurfaceOutput;
}
@@ -86,15 +87,15 @@
@Override
public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
- mSurfaceOutput = surfaceOutput;
- mOutputSurface = surfaceOutput.getSurface(mExecutor,
+ mSurfaceOutputs.put(surfaceOutput.getTargets(), surfaceOutput);
+ mOutputSurfaces.put(surfaceOutput.getTargets(), surfaceOutput.getSurface(mExecutor,
output -> {
if (mAutoCloseSurfaceOutput) {
surfaceOutput.close();
}
- mIsOutputSurfaceRequestedToClose = true;
+ mIsOutputSurfaceRequestedToClose.put(surfaceOutput.getTargets(), true);
}
- );
+ ));
}
@Nullable
@@ -102,9 +103,9 @@
return mSurfaceRequest;
}
- @Nullable
- public SurfaceOutput getSurfaceOutput() {
- return mSurfaceOutput;
+ @NonNull
+ public Map<Integer, SurfaceOutput> getSurfaceOutputs() {
+ return mSurfaceOutputs;
}
@NonNull
@@ -113,15 +114,16 @@
}
@NonNull
- public Surface getOutputSurface() {
- return mOutputSurface;
+ public Map<Integer, Surface> getOutputSurfaces() {
+ return mOutputSurfaces;
}
public boolean isInputSurfaceReleased() {
return mIsInputSurfaceReleased;
}
- public boolean isOutputSurfaceRequestedToClose() {
+ @NonNull
+ public Map<Integer, Boolean> isOutputSurfaceRequestedToClose() {
return mIsOutputSurfaceRequestedToClose;
}
@@ -129,8 +131,8 @@
* Clear up the instance to avoid the "{@link DeferrableSurface} garbage collected" error.
*/
public void cleanUp() {
- if (mSurfaceOutput != null) {
- mSurfaceOutput.close();
+ for (SurfaceOutput surfaceOutput : mSurfaceOutputs.values()) {
+ surfaceOutput.close();
}
}
}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
index ebf6f60..451594f 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
@@ -20,8 +20,10 @@
import static com.google.common.truth.Truth.assertThat;
import android.os.Build;
+import android.util.Size;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.impl.ImageFormatConstants;
import org.junit.Before;
import org.junit.Test;
@@ -30,6 +32,9 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -55,4 +60,17 @@
public void canRetrieveSensorRotation() {
assertThat(mFakeCameraInfo.getSensorRotationDegrees()).isEqualTo(SENSOR_ROTATION_DEGREES);
}
+
+ @Test
+ public void canRetrieveSupportedResolutions() {
+ List<Size> resolutions = new ArrayList<>();
+ resolutions.add(new Size(1280, 720));
+ resolutions.add(new Size(640, 480));
+ mFakeCameraInfo.setSupportedResolutions(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, resolutions);
+
+ assertThat(mFakeCameraInfo.getSupportedResolutions(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE))
+ .containsExactlyElementsIn(resolutions);
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 3b47dfb..9da1b86 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -99,7 +99,6 @@
import androidx.camera.core.internal.ThreadConfig;
import androidx.camera.core.processing.DefaultSurfaceProcessor;
import androidx.camera.core.processing.SettableSurface;
-import androidx.camera.core.processing.SurfaceEdge;
import androidx.camera.core.processing.SurfaceProcessorInternal;
import androidx.camera.core.processing.SurfaceProcessorNode;
import androidx.camera.video.StreamInfo.StreamState;
@@ -531,9 +530,13 @@
getRelativeRotation(camera),
/*mirroring=*/false,
onSurfaceInvalidated);
- SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
- SurfaceEdge outputEdge = mNode.transform(inputEdge);
- SettableSurface appSurface = outputEdge.getSurfaces().get(0);
+ SurfaceProcessorNode.OutConfig outConfig =
+ SurfaceProcessorNode.OutConfig.of(cameraSurface);
+ SurfaceProcessorNode.In nodeInput = SurfaceProcessorNode.In.of(
+ cameraSurface,
+ singletonList(outConfig));
+ SurfaceProcessorNode.Out nodeOutput = mNode.transform(nodeInput);
+ SettableSurface appSurface = requireNonNull(nodeOutput.get(outConfig));
mSurfaceRequest = appSurface.createSurfaceRequest(camera, targetFpsRange);
mDeferrableSurface = cameraSurface;
cameraSurface.getTerminationFuture().addListener(() -> {
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 2f15a2a..adfd01e 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -24,6 +24,7 @@
import android.util.Size
import android.view.Surface
import androidx.arch.core.util.Function
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.CameraXConfig
@@ -583,9 +584,9 @@
addAndAttachUseCases(videoCapture)
// Assert: surfaceOutput received.
- assertThat(processor.surfaceOutput).isNotNull()
+ assertThat(processor.surfaceOutputs).hasSize(1)
assertThat(processor.isReleased).isFalse()
- assertThat(processor.isOutputSurfaceRequestedToClose).isFalse()
+ assertThat(processor.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isNull()
assertThat(processor.isInputSurfaceReleased).isFalse()
assertThat(appSurfaceReadyToRelease).isFalse()
// processor surface is provided to camera.
@@ -597,12 +598,12 @@
// Assert: processor and processor surface is released.
assertThat(processor.isReleased).isTrue()
- assertThat(processor.isOutputSurfaceRequestedToClose).isTrue()
+ assertThat(processor.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isTrue()
assertThat(processor.isInputSurfaceReleased).isTrue()
assertThat(appSurfaceReadyToRelease).isFalse()
// Act: close SurfaceOutput
- processor.surfaceOutput!!.close()
+ processor.surfaceOutputs[VIDEO_CAPTURE]!!.close()
shadowOf(Looper.getMainLooper()).idle()
assertThat(appSurfaceReadyToRelease).isTrue()
}
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index cc0c1c7..53e7b06 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -54,6 +54,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
lintPublish project(":compose:animation:animation-lint")
@@ -107,6 +108,7 @@
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
+ implementation(libs.truth)
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index d6c980c..b23c1f5 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
@@ -43,8 +44,10 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -52,6 +55,8 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -59,7 +64,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
@LargeTest
@@ -242,15 +246,19 @@
) {
if (it) {
Box(
- modifier = Modifier.onGloballyPositioned {
- offset1 = it.positionInRoot()
- }.size(size1.width.dp, size1.height.dp)
+ modifier = Modifier
+ .onGloballyPositioned {
+ offset1 = it.positionInRoot()
+ }
+ .size(size1.width.dp, size1.height.dp)
)
} else {
Box(
- modifier = Modifier.onGloballyPositioned {
- offset2 = it.positionInRoot()
- }.size(size2.width.dp, size2.height.dp)
+ modifier = Modifier
+ .onGloballyPositioned {
+ offset2 = it.positionInRoot()
+ }
+ .size(size2.width.dp, size2.height.dp)
)
}
}
@@ -334,16 +342,19 @@
}
}
- @OptIn(ExperimentalAnimationApi::class, InternalAnimationApi::class)
+ @OptIn(ExperimentalAnimationApi::class)
@Test
fun AnimatedContentSlideInAndOutOfContainerTest() {
- val transitionState = MutableTransitionState(true).apply { targetState = false }
+ val transitionState = MutableTransitionState(true)
+ // LinearEasing is required to ensure the animation doesn't reach final values before the
+ // duration.
val animSpec = tween<IntOffset>(200, easing = LinearEasing)
+ lateinit var trueTransition: Transition<EnterExitState>
+ lateinit var falseTransition: Transition<EnterExitState>
+ rule.mainClock.autoAdvance = false
rule.setContent {
CompositionLocalProvider(LocalDensity provides Density(1f, 1f)) {
- if (!transitionState.targetState && !transitionState.currentState) {
- transitionState.targetState = true
- }
+ @Suppress("UpdateTransitionLabel")
val rootTransition = updateTransition(transitionState)
rootTransition.AnimatedContent(
transitionSpec = {
@@ -365,33 +376,65 @@
}
}
) { target ->
- Box(Modifier.requiredSize(200.dp))
- LaunchedEffect(transitionState.targetState) {
- while (transition.animations.size == 0) {
- delay(10)
- }
- val anim = transition.animations[0]
- while (transitionState.currentState != transitionState.targetState) {
- val playTime = (transition.playTimeNanos / 1000_000L).toInt()
- if (!transitionState.targetState) {
- if (target) {
- assertEquals(IntOffset(-playTime, 0), anim.value)
- } else {
- assertEquals(IntOffset(200 - playTime, 0), anim.value)
- }
- } else {
- if (target) {
- assertEquals(IntOffset(playTime - 200, 0), anim.value)
- } else {
- assertEquals(IntOffset(playTime, 0), anim.value)
- }
- }
- delay(10)
- }
+ if (target) {
+ trueTransition = transition
+ } else {
+ falseTransition = transition
}
+ Box(
+ Modifier
+ .requiredSize(200.dp)
+ .testTag(target.toString())
+ )
}
}
}
+
+ // Kick off the first animation.
+ transitionState.targetState = false
+ // The initial composition creates the transition…
+ rule.mainClock.advanceTimeByFrame()
+ rule.onNodeWithTag("true").assertExists()
+ rule.onNodeWithTag("false").assertExists()
+ // …but the animation won't actually start until one frame later.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(trueTransition.animations).isNotEmpty()
+ assertThat(falseTransition.animations).isNotEmpty()
+
+ // Loop to ensure the content is offset correctly at each frame.
+ var trueAnim = trueTransition.animations[0]
+ var falseAnim = falseTransition.animations[0]
+ assertThat(transitionState.currentState).isTrue()
+ while (transitionState.currentState) {
+ // True is leaving: it should start at 0 and slide out to -200.
+ assertThat(trueAnim.value).isEqualTo(IntOffset(-trueTransition.playTimeMillis, 0))
+ // False is entering: it should start at 200 and slide in to 0.
+ assertThat(falseAnim.value)
+ .isEqualTo(IntOffset(200 - falseTransition.playTimeMillis, 0))
+ rule.mainClock.advanceTimeByFrame()
+ }
+ // The animation should remove the newly-hidden node from the composition.
+ rule.onNodeWithTag("true").assertDoesNotExist()
+
+ // Kick off the second transition.
+ transitionState.targetState = true
+ rule.mainClock.advanceTimeByFrame()
+ rule.onNodeWithTag("true").assertExists()
+ rule.onNodeWithTag("false").assertExists()
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(trueTransition.animations).isNotEmpty()
+
+ trueAnim = trueTransition.animations[0]
+ falseAnim = falseTransition.animations[0]
+ assertThat(transitionState.currentState).isFalse()
+ while (!transitionState.currentState) {
+ // True is entering, it should start at -200 and slide in to 0.
+ assertThat(trueAnim.value).isEqualTo(IntOffset(trueTransition.playTimeMillis - 200, 0))
+ // False is leaving, it should start at 0 and slide out to 200.
+ assertThat(falseAnim.value).isEqualTo(IntOffset(falseTransition.playTimeMillis, 0))
+ rule.mainClock.advanceTimeByFrame()
+ }
+ rule.onNodeWithTag("false").assertDoesNotExist()
}
@OptIn(ExperimentalAnimationApi::class)
@@ -488,4 +531,7 @@
flag = false
}
}
+
+ @OptIn(InternalAnimationApi::class)
+ private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
}
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index 94a0da5..dab4a12 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -44,8 +44,10 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -53,6 +55,7 @@
import androidx.compose.ui.util.lerp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -95,7 +98,8 @@
) { fullSize -> IntSize(fullSize.width / 10, fullSize.height / 5) },
) {
Box(
- Modifier.requiredSize(100.dp, 100.dp)
+ Modifier
+ .requiredSize(100.dp, 100.dp)
.onGloballyPositioned {
offset = it.localToRoot(Offset.Zero)
}
@@ -204,7 +208,8 @@
) { fullSize -> IntOffset(-fullSize.width / 10, fullSize.height / 5) },
) {
Box(
- Modifier.requiredSize(100.dp, 100.dp)
+ Modifier
+ .requiredSize(100.dp, 100.dp)
.onGloballyPositioned {
offset = it.localToRoot(Offset.Zero)
}
@@ -346,7 +351,11 @@
enter = fadeIn(animationSpec = tween(500, easing = easing)),
exit = fadeOut(animationSpec = tween(300, easing = easingOut)),
) {
- Box(modifier = Modifier.size(size = 20.dp).background(Color.White))
+ Box(
+ modifier = Modifier
+ .size(size = 20.dp)
+ .background(Color.White)
+ )
LaunchedEffect(visible) {
var exit = false
val enterExit = transition
@@ -421,7 +430,11 @@
enter = scaleIn(animationSpec = tween(500, easing = easing)),
exit = scaleOut(animationSpec = tween(300, easing = easingOut)),
) {
- Box(modifier = Modifier.size(size = 20.dp).background(Color.White))
+ Box(
+ modifier = Modifier
+ .size(size = 20.dp)
+ .background(Color.White)
+ )
LaunchedEffect(visible) {
var exit = false
val enterExit = transition
@@ -544,9 +557,11 @@
transition.AnimatedVisibility(
// Only visible in State2
visible = { it == TestState.State2 },
- modifier = testModifier,
- enter = expandIn(animationSpec = tween(100)),
- exit = shrinkOut(animationSpec = tween(100))
+ modifier = testModifier.testTag("content"),
+ // Must use LinearEasing otherwise the target size will be reached before the
+ // animation finishes running.
+ enter = expandIn(animationSpec = tween(100, easing = LinearEasing)),
+ exit = shrinkOut(animationSpec = tween(100, easing = LinearEasing))
) {
Box(Modifier.requiredSize(100.dp, 100.dp)) {
DisposableEffect(Unit) {
@@ -559,29 +574,30 @@
}
}
rule.runOnIdle {
- assertEquals(0, testModifier.width)
- assertEquals(0, testModifier.height)
+ assertThat(testModifier.width).isEqualTo(0)
+ assertThat(testModifier.height).isEqualTo(0)
testState.value = TestState.State2
}
while (currentState != TestState.State2) {
- assertTrue(testModifier.width < 100)
+ assertThat(testModifier.width).isLessThan(100)
rule.mainClock.advanceTimeByFrame()
}
rule.runOnIdle {
- assertEquals(100, testModifier.width)
- assertEquals(100, testModifier.height)
+ assertThat(testModifier.width).isEqualTo(100)
+ assertThat(testModifier.height).isEqualTo(100)
testState.value = TestState.State3
}
while (currentState != TestState.State3) {
- assertTrue(testModifier.width > 0)
- assertFalse(disposed)
+ assertThat(testModifier.width).isGreaterThan(0)
+ rule.onNodeWithTag("content").assertExists()
+ assertThat(disposed).isFalse()
rule.mainClock.advanceTimeByFrame()
}
- rule.mainClock.advanceTimeByFrame()
+ // When the hide animation finishes, it will never get measured with size 0 because the
+ // animation will remove it from the composition instead.
+ rule.onNodeWithTag("content").assertDoesNotExist()
rule.runOnIdle {
- assertEquals(0, testModifier.width)
- assertEquals(0, testModifier.height)
- assertTrue(disposed)
+ assertThat(disposed).isTrue()
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
index a3a9ef1..2293c1a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -47,6 +47,7 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.IterableSubject
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -147,6 +148,7 @@
)
}
+ @SdkSuppress(minSdkVersion = 29) // b/260010883
@Test
fun verifyScrollActionsAtEnd() {
createScrollableContent_StartAtEnd()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
index 87f9928..a29edf6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
@@ -54,6 +54,7 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.IterableSubject
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -154,6 +155,7 @@
)
}
+ @SdkSuppress(minSdkVersion = 29) // b/260011449
@Test
fun verifyScrollActionsAtEnd() {
createScrollableContent_StartAtEnd()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
new file mode 100644
index 0000000..992c360
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -0,0 +1,311 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+
+@OptIn(ExperimentalFoundationApi::class)
+internal open class BasePagerTest(private val config: ParamConfig) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val isVertical = config.orientation == Orientation.Vertical
+ lateinit var scope: CoroutineScope
+ var pagerSize: Int = 0
+ var placed = mutableSetOf<Int>()
+ var pageSize: Int = 0
+
+ @Stable
+ fun Modifier.crossAxisSize(size: Dp) =
+ if (isVertical) {
+ this.width(size)
+ } else {
+ this.height(size)
+ }
+
+ fun TouchInjectionScope.swipeWithVelocityAcrossMainAxis(velocity: Float, delta: Float? = null) {
+ val end = if (delta == null) {
+ layoutEnd
+ } else {
+ if (isVertical) {
+ layoutStart.copy(y = layoutStart.y + delta)
+ } else {
+ layoutStart.copy(x = layoutStart.x + delta)
+ }
+ }
+ swipeWithVelocity(layoutStart, end, velocity)
+ }
+
+ internal fun createPager(
+ state: PagerState,
+ modifier: Modifier = Modifier,
+ pagerCount: () -> Int = { DefaultPageCount },
+ offscreenPageLimit: Int = 0,
+ pageSize: PageSize = PageSize.Fill,
+ userScrollEnabled: Boolean = true,
+ snappingPage: PagerSnapDistance = PagerSnapDistance.atMost(1),
+ effects: @Composable () -> Unit = {}
+ ) {
+ rule.setContent {
+ val flingBehavior =
+ PagerDefaults.flingBehavior(
+ state = state,
+ pagerSnapDistance = snappingPage
+ )
+ CompositionLocalProvider(LocalLayoutDirection provides config.layoutDirection) {
+ scope = rememberCoroutineScope()
+ HorizontalOrVerticalPager(
+ pageCount = pagerCount(),
+ state = state,
+ beyondBoundsPageCount = offscreenPageLimit,
+ modifier = modifier
+ .testTag(PagerTestTag)
+ .onSizeChanged { pagerSize = if (isVertical) it.height else it.width },
+ pageSize = pageSize,
+ userScrollEnabled = userScrollEnabled,
+ reverseLayout = config.reverseLayout,
+ flingBehavior = flingBehavior,
+ pageSpacing = config.pageSpacing,
+ contentPadding = config.mainAxisContentPadding
+ ) {
+ Page(index = it)
+ }
+ effects()
+ }
+ }
+ }
+
+ @Composable
+ internal fun Page(index: Int) {
+ Box(modifier = Modifier
+ .onPlaced {
+ placed.add(index)
+ pageSize = if (isVertical) it.size.height else it.size.width
+ }
+ .fillMaxSize()
+ .background(Color.Blue)
+ .testTag("$index"),
+ contentAlignment = Alignment.Center) {
+ BasicText(text = index.toString())
+ }
+ }
+
+ internal fun onPager(): SemanticsNodeInteraction {
+ return rule.onNodeWithTag(PagerTestTag)
+ }
+
+ internal val scrollForwardSign: Int
+ get() = if (isVertical) {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ 1
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ -1
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ 1
+ } else {
+ -1
+ }
+ } else {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ -1
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ 1
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ 1
+ } else {
+ -1
+ }
+ }
+
+ internal val TouchInjectionScope.layoutStart: Offset
+ get() = if (isVertical) {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ topCenter
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ bottomCenter
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ topCenter
+ } else {
+ bottomCenter
+ }
+ } else {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ centerRight
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ centerLeft
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ centerLeft
+ } else {
+ centerRight
+ }
+ }
+
+ internal val TouchInjectionScope.layoutEnd: Offset
+ get() = if (isVertical) {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ bottomCenter
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ topCenter
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ bottomCenter
+ } else {
+ topCenter
+ }
+ } else {
+ if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ centerLeft
+ } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+ centerRight
+ } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+ centerRight
+ } else {
+ centerLeft
+ }
+ }
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ internal fun HorizontalOrVerticalPager(
+ pageCount: Int,
+ state: PagerState = rememberPagerState(),
+ modifier: Modifier = Modifier,
+ userScrollEnabled: Boolean = true,
+ reverseLayout: Boolean = false,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ beyondBoundsPageCount: Int = 0,
+ pageSize: PageSize = PageSize.Fill,
+ flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+ pageSpacing: Dp = 0.dp,
+ pageContent: @Composable (pager: Int) -> Unit
+ ) {
+ if (isVertical) {
+ VerticalPager(
+ pageCount = pageCount,
+ state = state,
+ modifier = modifier,
+ userScrollEnabled = userScrollEnabled,
+ reverseLayout = reverseLayout,
+ contentPadding = contentPadding,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pageSize = pageSize,
+ flingBehavior = flingBehavior,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent
+ )
+ } else {
+ HorizontalPager(
+ pageCount = pageCount,
+ state = state,
+ modifier = modifier,
+ userScrollEnabled = userScrollEnabled,
+ reverseLayout = reverseLayout,
+ contentPadding = contentPadding,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pageSize = pageSize,
+ flingBehavior = flingBehavior,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent
+ )
+ }
+ }
+
+ internal fun confirmPageIsInCorrectPosition(
+ currentPageIndex: Int,
+ pageToVerifyPosition: Int = currentPageIndex
+ ) {
+ val leftContentPadding =
+ config.mainAxisContentPadding.calculateLeftPadding(config.layoutDirection)
+ val topContentPadding = config.mainAxisContentPadding.calculateTopPadding()
+
+ val (left, top) = with(rule.density) {
+ val spacings = config.pageSpacing.roundToPx()
+ val initialPageOffset = currentPageIndex * (pageSize + spacings)
+
+ val position = pageToVerifyPosition * (pageSize + spacings) - initialPageOffset
+ if (isVertical) {
+ 0.dp to position.toDp()
+ } else {
+ position.toDp() to 0.dp
+ }
+ }
+ rule.onNodeWithTag("$pageToVerifyPosition")
+ .assertPositionInRootIsEqualTo(left + leftContentPadding, top + topContentPadding)
+ }
+}
+
+internal class ParamConfig(
+ val orientation: Orientation,
+ val reverseLayout: Boolean = false,
+ val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+ val pageSpacing: Dp = 0.dp,
+ val mainAxisContentPadding: PaddingValues = PaddingValues(0.dp)
+) {
+ override fun toString(): String {
+ return "orientation=$orientation " +
+ "reverseLayout=$reverseLayout " +
+ "layoutDirection=$layoutDirection " +
+ "pageSpacing=$pageSpacing " +
+ "mainAxisContentPadding=$mainAxisContentPadding"
+ }
+}
+
+internal const val PagerTestTag = "pager"
+internal const val DefaultPageCount = 20
+internal val TestOrientation = listOf(Orientation.Vertical, Orientation.Horizontal)
+internal val TestReverseLayout = listOf(false, true)
+internal val TestLayoutDirection = listOf(LayoutDirection.Rtl, LayoutDirection.Ltr)
+internal val TestPageSpacing = listOf(0.dp, 8.dp)
+internal fun testContentPaddings(orientation: Orientation) = listOf(
+ PaddingValues(0.dp),
+ if (orientation == Orientation.Vertical)
+ PaddingValues(vertical = 16.dp)
+ else PaddingValues(horizontal = 16.dp),
+ PaddingValues(start = 16.dp),
+ PaddingValues(end = 16.dp)
+)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
new file mode 100644
index 0000000..776098c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class EmptyPagerTests(val config: ParamConfig) : BasePagerTest(config) {
+
+ @Test
+ fun checkNoPagesArePlaced() {
+ // Arrange
+ val state = PagerState()
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize(), pagerCount = { 0 })
+
+ // Assert
+ rule.onNodeWithTag("0").assertDoesNotExist()
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ add(ParamConfig(orientation = orientation))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
new file mode 100644
index 0000000..9f13bea
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerCrossAxisTest(val config: ParamConfig) : BasePagerTest(config) {
+
+ @Test
+ fun pagerOnInfiniteCrossAxisLayout_shouldWrapContentSize() {
+ // Arrange
+ rule.setContent {
+ InfiniteAxisRootComposable {
+ HorizontalOrVerticalPager(
+ pageCount = DefaultPageCount,
+ state = rememberPagerState(),
+ modifier = Modifier
+ .fillMaxHeight()
+ .fillMaxWidth()
+ .testTag(PagerTestTag),
+ ) {
+ val fillModifier = if (isVertical) {
+ Modifier
+ .fillMaxHeight()
+ .width(200.dp)
+ } else {
+ Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ }
+ Box(fillModifier)
+ }
+ }
+ }
+
+ // Act
+ val rootBounds = rule.onRoot().getUnclippedBoundsInRoot()
+
+ // Assert: Max Cross Axis size is handled well by wrapping content
+ if (isVertical) {
+ rule.onNodeWithTag(PagerTestTag)
+ .assertHeightIsEqualTo(rootBounds.height)
+ .assertWidthIsEqualTo(200.dp)
+ } else {
+ rule.onNodeWithTag(PagerTestTag)
+ .assertWidthIsEqualTo(rootBounds.width)
+ .assertHeightIsEqualTo(200.dp)
+ }
+ }
+
+ @Composable
+ private fun InfiniteAxisRootComposable(content: @Composable () -> Unit) {
+ if (isVertical) {
+ Row(Modifier.horizontalScroll(rememberScrollState())) {
+ content()
+ }
+ } else {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ content()
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ add(ParamConfig(orientation = orientation))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
new file mode 100644
index 0000000..6e02eab
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerScrollingTest(
+ val config: ParamConfig
+) : BasePagerTest(config) {
+
+ @Test
+ fun swipePageTowardsEdge_shouldNotMove() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act - backward
+ rule.onNodeWithTag("0").performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta * -1.0f
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("0").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(0)
+
+ // Act - forward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("1").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(1)
+ }
+
+ @Test
+ fun swipeAllTheWay_verifyPagesAreLayedOutCorrectly() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act and Assert - forward
+ repeat(DefaultPageCount) {
+ rule.onNodeWithTag(it.toString()).assertIsDisplayed()
+ confirmPageIsInCorrectPosition(it)
+ rule.onNodeWithTag(it.toString()).performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+ }
+
+ // Act - backward
+ repeat(DefaultPageCount) {
+ val countDown = DefaultPageCount - 1 - it
+ rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
+ confirmPageIsInCorrectPosition(countDown)
+ rule.onNodeWithTag(countDown.toString()).performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+ delta * -1f
+ )
+ }
+ rule.waitForIdle()
+ }
+ }
+
+ @Test
+ fun swipeWithLowVelocity_shouldBounceBack() {
+ // Arrange
+ val state = PagerState(5)
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act - forward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(5)
+
+ // Act - backward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+ delta * -1
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(5)
+ }
+
+ @Test
+ fun swipeWithHighVelocity_shouldGoToNextPage() {
+ // Arrange
+ val state = PagerState(5)
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ // make sure the scroll distance is not enough to go to next page
+ val delta = pagerSize * 0.4f * scrollForwardSign
+
+ // Act - forward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+ delta
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("6").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(6)
+
+ // Act - backward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(
+ with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+ delta * -1
+ )
+ }
+ rule.waitForIdle()
+
+ // Assert
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(5)
+ }
+
+ @Test
+ fun scrollWithoutVelocity_shouldSettlingInClosestPage() {
+ // Arrange
+ val state = PagerState(5)
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ // This will scroll 1 whole page before flinging
+ val delta = pagerSize * 1.4f * scrollForwardSign
+
+ // Act - forward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(0f, delta)
+ }
+ rule.waitForIdle()
+
+ // Assert
+ assertThat(state.currentPage).isAtMost(7)
+ rule.onNodeWithTag("${state.currentPage}").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(state.currentPage)
+
+ // Act - backward
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(0f, delta * -1)
+ }
+ rule.waitForIdle()
+
+ // Assert
+ assertThat(state.currentPage).isAtLeast(5)
+ rule.onNodeWithTag("${state.currentPage}").assertIsDisplayed()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun offscreenPageLimitIsUsed_shouldPlaceMoreItemsThanVisibleOnesAsWeScroll() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 1)
+ val delta = pagerSize * 1.4f * scrollForwardSign
+
+ repeat(DefaultPageCount) {
+ // Act
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(0f, delta)
+ }
+
+ rule.waitForIdle()
+ // Next page was placed
+ rule.runOnIdle {
+ assertThat(placed).contains(
+ (state.currentPage + 1)
+ .coerceAtMost(DefaultPageCount - 1)
+ )
+ }
+ }
+ rule.waitForIdle()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ for (reverseLayout in TestReverseLayout) {
+ for (layoutDirection in TestLayoutDirection) {
+ for (pageSpacing in TestPageSpacing) {
+ for (contentPadding in testContentPaddings(orientation)) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ reverseLayout = reverseLayout,
+ layoutDirection = layoutDirection,
+ pageSpacing = pageSpacing,
+ mainAxisContentPadding = contentPadding
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
new file mode 100644
index 0000000..e302c0c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -0,0 +1,530 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerStateTest(val config: ParamConfig) : BasePagerTest(config) {
+
+ @Test
+ fun scrollToPage_shouldPlacePagesCorrectly() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act and Assert
+ repeat(DefaultPageCount) {
+ assertThat(state.currentPage).isEqualTo(it)
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(state.currentPage + 1)
+ }
+ }
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+ }
+
+ @Test
+ fun scrollToPage_longSkipShouldNotPlaceIntermediatePages() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act
+ assertThat(state.currentPage).isEqualTo(0)
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(DefaultPageCount - 1)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+ }
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun animateScrollToPage_shouldPlacePagesCorrectly() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act and Assert
+ repeat(DefaultPageCount) {
+ assertThat(state.currentPage).isEqualTo(it)
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(state.currentPage + 1)
+ }
+ }
+ rule.waitForIdle()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+ }
+
+ @Test
+ fun animateScrollToPage_longSkipShouldNotPlaceIntermediatePages() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act
+ assertThat(state.currentPage).isEqualTo(0)
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(DefaultPageCount - 1)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2)
+ assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+ }
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun scrollToPage_shouldCoerceWithinRange() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act
+ assertThat(state.currentPage).isEqualTo(0)
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(DefaultPageCount)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle { assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+ // Act
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(-1)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle { assertThat(state.currentPage).isEqualTo(0) }
+ }
+
+ @Test
+ fun animateScrollToPage_shouldCoerceWithinRange() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Act
+ assertThat(state.currentPage).isEqualTo(0)
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(DefaultPageCount)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle { assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+ // Act
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(-1)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle { assertThat(state.currentPage).isEqualTo(0) }
+ }
+
+ @Test
+ fun animateScrollToPage_withPassedAnimation() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ val differentAnimation: AnimationSpec<Float> = tween()
+
+ // Act and Assert
+ repeat(DefaultPageCount) {
+ assertThat(state.currentPage).isEqualTo(it)
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(state.currentPage + 1, differentAnimation)
+ }
+ }
+ rule.waitForIdle()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+ }
+
+ @Test
+ fun currentPage_shouldChangeWhenClosestPageToSnappedPositionChanges() {
+ // Arrange
+ val state = PagerState()
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+ var previousCurrentPage = state.currentPage
+
+ // Act
+ // Move less than half an item
+ val firstDelta = (pagerSize * 0.4f) * scrollForwardSign
+ onPager().performTouchInput {
+ down(layoutStart)
+ if (isVertical) {
+ moveBy(Offset(0f, firstDelta))
+ } else {
+ moveBy(Offset(firstDelta, 0f))
+ }
+ }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(previousCurrentPage)
+ }
+ // Release pointer
+ onPager().performTouchInput { up() }
+
+ rule.runOnIdle {
+ previousCurrentPage = state.currentPage
+ }
+ confirmPageIsInCorrectPosition(state.currentPage)
+
+ // Arrange
+ // Pass closest to snap position threshold (over half an item)
+ val secondDelta = (pagerSize * 0.6f) * scrollForwardSign
+
+ // Act
+ onPager().performTouchInput {
+ down(layoutStart)
+ if (isVertical) {
+ moveBy(Offset(0f, secondDelta))
+ } else {
+ moveBy(Offset(secondDelta, 0f))
+ }
+ }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(previousCurrentPage + 1)
+ }
+
+ onPager().performTouchInput { up() }
+ rule.waitForIdle()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun targetPage_performScroll_shouldShowNextPage() {
+ // Arrange
+ val state = PagerState()
+ createPager(
+ state = state,
+ modifier = Modifier.fillMaxSize(),
+ snappingPage = PagerSnapDistance.atMost(3)
+ )
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving forward
+ val forwardDelta = pagerSize * 0.4f * scrollForwardSign.toFloat()
+ onPager().performTouchInput {
+ down(layoutStart)
+ moveBy(Offset(forwardDelta, forwardDelta))
+ }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(state.currentPage + 1)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ // Reset
+ rule.mainClock.autoAdvance = true
+ onPager().performTouchInput { up() }
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ rule.runOnIdle {
+ runBlocking { state.scrollToPage(5) }
+ }
+
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving backward
+ val backwardDelta = -pagerSize * 0.4f * scrollForwardSign.toFloat()
+ onPager().performTouchInput {
+ down(layoutEnd)
+ moveBy(Offset(backwardDelta, backwardDelta))
+ }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(state.currentPage - 1)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ rule.mainClock.autoAdvance = true
+ onPager().performTouchInput { up() }
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ }
+
+ @Test
+ fun targetPage_performingFling_shouldGoToPredictedPage() {
+ // Arrange
+ val state = PagerState()
+ createPager(
+ state = state,
+ modifier = Modifier.fillMaxSize(),
+ snappingPage = PagerSnapDistance.atMost(3)
+ )
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving forward
+ var previousTarget = state.targetPage
+ val forwardDelta = pagerSize * scrollForwardSign.toFloat()
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(20000f, forwardDelta)
+ }
+ rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(state.currentPage + 3)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ rule.mainClock.autoAdvance = true
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving backward
+ previousTarget = state.targetPage
+ val backwardDelta = -pagerSize * scrollForwardSign.toFloat()
+ onPager().performTouchInput {
+ swipeWithVelocityAcrossMainAxis(20000f, backwardDelta)
+ }
+ rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(state.currentPage - 3)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ rule.mainClock.autoAdvance = true
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ }
+
+ @Test
+ fun targetPage_shouldReflectTargetWithAnimation() {
+ // Arrange
+ val state = PagerState()
+ createPager(
+ state = state,
+ modifier = Modifier.fillMaxSize()
+ )
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving forward
+ var previousTarget = state.targetPage
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(DefaultPageCount - 1)
+ }
+ }
+ rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(DefaultPageCount - 1)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ rule.mainClock.autoAdvance = true
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ rule.mainClock.autoAdvance = false
+
+ // Act
+ // Moving backward
+ previousTarget = state.targetPage
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(0)
+ }
+ }
+ rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+ // Assert
+ assertThat(state.targetPage).isEqualTo(0)
+ assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+ rule.mainClock.autoAdvance = true
+ rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+ }
+
+ @Test
+ fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
+ // Arrange
+ val state = PagerState(DefaultPageCount / 2)
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // No offset initially
+ rule.runOnIdle {
+ assertThat(state.currentPageOffset).isWithin(0.01f).of(0f)
+ }
+
+ // Act
+ // Moving forward
+ onPager().performTouchInput {
+ down(layoutStart)
+ if (isVertical) {
+ moveBy(Offset(0f, scrollForwardSign * pagerSize / 4f))
+ } else {
+ moveBy(Offset(scrollForwardSign * pagerSize / 4f, 0f))
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.currentPageOffset).isWithin(0.1f).of(0.25f)
+ }
+
+ onPager().performTouchInput { up() }
+ rule.waitForIdle()
+
+ // Reset
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(DefaultPageCount / 2)
+ }
+ }
+
+ // No offset initially (again)
+ rule.runOnIdle {
+ assertThat(state.currentPageOffset).isWithin(0.01f).of(0f)
+ }
+
+ // Act
+ // Moving backward
+ onPager().performTouchInput {
+ down(layoutStart)
+ if (isVertical) {
+ moveBy(Offset(0f, -scrollForwardSign * pagerSize / 4f))
+ } else {
+ moveBy(Offset(-scrollForwardSign * pagerSize / 4f, 0f))
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.currentPageOffset).isWithin(0.1f).of(-0.25f)
+ }
+ }
+
+ @Test
+ fun initialPageOnPagerState_shouldDisplayThatPageFirst() {
+ // Arrange
+ val state = PagerState(5)
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Assert
+ rule.onNodeWithTag("4").assertDoesNotExist()
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ rule.onNodeWithTag("6").assertDoesNotExist()
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun testStateRestoration() {
+ // Arrange
+ val tester = StateRestorationTester(rule)
+ val state = PagerState()
+ tester.setContent {
+ scope = rememberCoroutineScope()
+ HorizontalOrVerticalPager(
+ state = state,
+ pageCount = DefaultPageCount,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Page(it)
+ }
+ }
+
+ // Act
+ rule.runOnIdle {
+ scope.launch {
+ state.scrollToPage(5)
+ }
+ runBlocking {
+ state.scroll {
+ scrollBy(50f)
+ }
+ }
+ }
+
+ // TODO(levima) Update to assert offset as well
+ val previousPage = state.currentPage
+ tester.emulateSavedInstanceStateRestore()
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(previousPage)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ for (reverseLayout in TestReverseLayout) {
+ for (layoutDirection in TestLayoutDirection) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ reverseLayout = reverseLayout,
+ layoutDirection = layoutDirection
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
new file mode 100644
index 0000000..e88a163
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
@@ -0,0 +1,267 @@
+/*
+ * 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerTest(val config: ParamConfig) : BasePagerTest(config) {
+
+ @Before
+ fun setUp() {
+ placed.clear()
+ }
+
+ @Test
+ fun userScrollEnabledIsOff_shouldNotAllowGestureScroll() {
+ // Arrange
+ val state = PagerState()
+ createPager(
+ state = state,
+ userScrollEnabled = false,
+ modifier = Modifier.fillMaxSize()
+ )
+
+ // Act
+ onPager().performTouchInput { swipeWithVelocityAcrossMainAxis(1000f) }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(0)
+ }
+
+ confirmPageIsInCorrectPosition(0, 0)
+ }
+
+ @Test
+ fun userScrollEnabledIsOff_shouldAllowAnimationScroll() {
+ // Arrange
+ val state = PagerState()
+ createPager(
+ state = state,
+ userScrollEnabled = false,
+ modifier = Modifier.fillMaxSize()
+ )
+
+ // Act
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(5)
+ }
+ }
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(state.currentPage).isEqualTo(5)
+ }
+ confirmPageIsInCorrectPosition(5)
+ }
+
+ @Test
+ fun userScrollEnabledIsOn_shouldAllowGestureScroll() {
+ // Arrange
+ val state = PagerState(5)
+ createPager(
+ state = state,
+ userScrollEnabled = true,
+ modifier = Modifier.fillMaxSize()
+ )
+
+ onPager().performTouchInput { swipeWithVelocityAcrossMainAxis(1000f) }
+
+ rule.runOnIdle {
+ assertThat(state.currentPage).isNotEqualTo(5)
+ }
+ confirmPageIsInCorrectPosition(state.currentPage)
+ }
+
+ @Test
+ fun pageSizeFill_onlySnappedItemIsDisplayed() {
+ // Arrange
+ val state = PagerState(5)
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Assert
+ rule.onNodeWithTag("4").assertDoesNotExist()
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ rule.onNodeWithTag("6").assertDoesNotExist()
+ confirmPageIsInCorrectPosition(5)
+ }
+
+ @Test
+ fun pagerSizeCustom_visibleItemsAreWithinViewport() {
+ // Arrange
+ val state = PagerState(5)
+ val pagerMode = object : PageSize {
+ override fun Density.calculateMainAxisPageSize(
+ availableSpace: Int,
+ pageSpacing: Int
+ ): Int {
+ return 100.dp.roundToPx() + pageSpacing
+ }
+ }
+
+ // Act
+ createPager(
+ state = state,
+ modifier = Modifier.crossAxisSize(200.dp),
+ offscreenPageLimit = 0,
+ pageSize = pagerMode
+ )
+
+ // Assert
+ rule.runOnIdle {
+ val visibleItems = state.lazyListState.layoutInfo.visibleItemsInfo.size
+ val pageCount = with(rule.density) {
+ (pagerSize / (pageSize + config.pageSpacing.roundToPx()))
+ } + 1
+ assertThat(visibleItems).isEqualTo(pageCount)
+ }
+
+ for (pageIndex in 5 until state.lazyListState.layoutInfo.visibleItemsInfo.size + 4) {
+ confirmPageIsInCorrectPosition(5, pageIndex)
+ }
+ }
+
+ @Test
+ fun offscreenPageLimitIsUsed_shouldPlaceMoreItemsThanVisibleOnes() {
+ // Arrange
+ val initialIndex = 5
+ val state = PagerState(initialIndex)
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 2)
+
+ // Assert
+ rule.runOnIdle {
+ assertThat(placed).contains(initialIndex - 2)
+ assertThat(placed).contains(initialIndex - 1)
+ assertThat(placed).contains(initialIndex + 1)
+ assertThat(placed).contains(initialIndex + 2)
+ }
+ confirmPageIsInCorrectPosition(initialIndex, initialIndex - 2)
+ confirmPageIsInCorrectPosition(initialIndex, initialIndex - 1)
+ confirmPageIsInCorrectPosition(initialIndex, initialIndex + 1)
+ confirmPageIsInCorrectPosition(initialIndex, initialIndex + 2)
+ }
+
+ @Test
+ fun offscreenPageLimitIsNotUsed_shouldNotPlaceMoreItemsThanVisibleOnes() {
+ // Arrange
+ val state = PagerState(5)
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 0)
+
+ // Assert
+ rule.waitForIdle()
+ assertThat(placed).doesNotContain(4)
+ assertThat(placed).contains(5)
+ assertThat(placed).doesNotContain(6)
+ confirmPageIsInCorrectPosition(5)
+ }
+
+ @Test
+ fun pageCount_pagerOnlyContainsGivenPageCountItems() {
+ // Arrange
+ val state = PagerState()
+
+ // Act
+ createPager(state = state, modifier = Modifier.fillMaxSize())
+
+ // Assert
+ repeat(DefaultPageCount) {
+ rule.onNodeWithTag("$it").assertIsDisplayed()
+ rule.runOnIdle {
+ scope.launch {
+ state.scroll {
+ scrollBy(pagerSize.toFloat())
+ }
+ }
+ }
+ rule.waitForIdle()
+ }
+ rule.onNodeWithTag("$DefaultPageCount").assertDoesNotExist()
+ }
+
+ @Test
+ fun mutablePageCount_assertPagesAreChangedIfCountIsChanged() {
+ // Arrange
+ val state = PagerState()
+ val pageCount = mutableStateOf(2)
+ createPager(
+ state = state,
+ modifier = Modifier.fillMaxSize(),
+ pagerCount = { pageCount.value }
+ )
+
+ rule.onNodeWithTag("3").assertDoesNotExist()
+
+ // Act
+ pageCount.value = DefaultPageCount
+ rule.waitForIdle()
+
+ // Assert
+ repeat(DefaultPageCount) {
+ rule.onNodeWithTag("$it").assertIsDisplayed()
+ rule.runOnIdle {
+ scope.launch {
+ state.scroll {
+ scrollBy(pagerSize.toFloat())
+ }
+ }
+ }
+ rule.waitForIdle()
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = mutableListOf<ParamConfig>().apply {
+ for (orientation in TestOrientation) {
+ for (pageSpacing in TestPageSpacing) {
+ add(
+ ParamConfig(
+ orientation = orientation,
+ pageSpacing = pageSpacing
+ )
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 42fab8f..b28e202 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -27,6 +27,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.layout.Arrangement
@@ -205,6 +206,10 @@
)
}
+ val pagerFlingBehavior = remember(flingBehavior, state) {
+ PagerWrapperFlingBehavior(flingBehavior, state)
+ }
+
LaunchedEffect(density, state, pageSpacing) {
with(density) { state.pageSpacing = pageSpacing.roundToPx() }
}
@@ -246,7 +251,7 @@
modifier = Modifier,
state = state.lazyListState,
contentPadding = contentPadding,
- flingBehavior = flingBehavior,
+ flingBehavior = pagerFlingBehavior,
horizontalAlignment = horizontalAlignment,
horizontalArrangement = Arrangement.spacedBy(
pageSpacing,
@@ -497,4 +502,18 @@
return (finalOffset - initialOffset)
}
}
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class PagerWrapperFlingBehavior(
+ val originalFlingBehavior: SnapFlingBehavior,
+ val pagerState: PagerState
+) : FlingBehavior {
+ override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+ return with(originalFlingBehavior) {
+ performFling(initialVelocity) { remainingScrollOffset ->
+ pagerState.snapRemainingScrollOffset = remainingScrollOffset
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 2e89671..fe9416d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -37,8 +37,11 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMaxBy
import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
/**
* Creates and remember a [PagerState] to be used with a [Pager]
@@ -65,6 +68,8 @@
initialPageOffset: Int = 0
) : ScrollableState {
+ internal var snapRemainingScrollOffset by mutableStateOf(0f)
+
internal val lazyListState = LazyListState(initialPage, initialPageOffset)
internal var pageSpacing by mutableStateOf(0)
@@ -78,6 +83,16 @@
private val pageAvailableSpace: Int
get() = pageSize + pageSpacing
+ /**
+ * How far the current page needs to scroll so the target page is considered to be the next
+ * page.
+ */
+ private val positionThresholdFraction: Float
+ get() = with(lazyListState.density) {
+ val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
+ minThreshold / pageSize.toFloat()
+ }
+
internal val pageCount: Int
get() = lazyListState.layoutInfo.totalItemsCount
@@ -113,6 +128,8 @@
*/
val currentPage: Int by derivedStateOf { closestPageToSnappedPosition?.index ?: 0 }
+ private var animationTargetPage by mutableStateOf(-1)
+
private var settledPageState by mutableStateOf(initialPage)
/**
@@ -125,6 +142,31 @@
}
/**
+ * The page this [Pager] intends to settle to.
+ * During fling or animated scroll (from [animateScrollToPage] this will represent the page
+ * this pager intends to settle to. When no scroll is ongoing, this will be equal to
+ * [currentPage].
+ */
+ val targetPage: Int by derivedStateOf {
+ if (!isScrollInProgress) {
+ currentPage
+ } else if (animationTargetPage != -1) {
+ animationTargetPage
+ } else {
+ val offsetFromFling = snapRemainingScrollOffset
+ val offsetFromScroll =
+ if (abs(currentPageOffset) >= abs(positionThresholdFraction)) {
+ (abs(currentPageOffset) + 1) * pageAvailableSpace * currentPageOffset.sign
+ } else {
+ 0f
+ }
+ val pageDisplacement =
+ (offsetFromFling + offsetFromScroll).roundToInt() / pageAvailableSpace
+ (currentPage + pageDisplacement).coerceInPageRange()
+ }
+ }
+
+ /**
* Indicates how far the current page is to the snapped position, this will vary from
* -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
* of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
@@ -162,6 +204,8 @@
) {
if (page == currentPage) return
var currentPosition = currentPage
+ val targetPage = page.coerceInPageRange()
+ animationTargetPage = targetPage
// If our future page is too far off, that is, outside of the current viewport
val firstVisiblePageIndex = visiblePages.first().index
val lastVisiblePageIndex = visiblePages.last().index
@@ -179,13 +223,13 @@
currentPosition = preJumpPosition
}
- val targetPage = page.coerceInPageRange()
val targetOffset = targetPage * pageAvailableSpace
val currentOffset = currentPosition * pageAvailableSpace
val pageOffsetToSnappedPosition = distanceToSnapPosition
val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
lazyListState.animateScrollBy(displacement, animationSpec)
+ animationTargetPage = -1
}
override suspend fun scroll(
@@ -238,4 +282,5 @@
private const val MaxPageOffset = 0.5f
internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
{ _, _ -> 0f }
-private const val MaxPagesForAnimateScroll = 3
\ No newline at end of file
+private val DefaultPositionThreshold = 56.dp
+private const val MaxPagesForAnimateScroll = 3
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
index c2e3f2c..706b93a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
@@ -15,7 +15,6 @@
*/
package androidx.compose.material
-import android.os.Build
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@@ -115,7 +114,8 @@
}
@Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // captureToImage() requires API level 26
+ // @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // captureToImage() requires API level 26
+ @SdkSuppress(minSdkVersion = 28) // b/260004658
fun badge_noContent_shape() {
var errorColor = Color.Unspecified
rule.setMaterialContent {
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
index 2131748..277eb6b 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
@@ -38,6 +38,7 @@
import androidx.compose.ui.tooling.animation.states.TargetState
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
@@ -212,6 +213,7 @@
}
}
+ @SdkSuppress(minSdkVersion = 29) // b/260004689
@Test
fun clockWithAnimatedContent() {
val clock = createBooleanTransitionClockWithAnimatedContent()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index a7d7eba..03d5552 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -391,6 +391,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testCameraDistanceWithRotationY() {
+ if (Build.VERSION.SDK_INT == 28) return // b/260095151
val testTag = "parent"
rule.setContent {
Box(modifier = Modifier.testTag(testTag).wrapContentSize()) {
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 2e29520..f85f282 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -44,7 +44,7 @@
android {
defaultConfig {
- minSdkVersion 33
+ minSdkVersion 31
multiDexEnabled = true
}
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 24a6baa..d79149c 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -55,7 +55,7 @@
android {
namespace "androidx.core.uwb"
defaultConfig {
- minSdkVersion 33
+ minSdkVersion 31
multiDexEnabled = true
}
}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
index 2094c06..b946a89 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -132,7 +132,6 @@
{
androidx.core.uwb.backend.UwbDevice _arg0;
_arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
- data.enforceNoDataAvail();
this.onRangingInitialized(_arg0);
break;
}
@@ -142,7 +141,6 @@
_arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
androidx.core.uwb.backend.RangingPosition _arg1;
_arg1 = data.readTypedObject(androidx.core.uwb.backend.RangingPosition.CREATOR);
- data.enforceNoDataAvail();
this.onRangingResult(_arg0, _arg1);
break;
}
@@ -152,7 +150,6 @@
_arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
int _arg1;
_arg1 = data.readInt();
- data.enforceNoDataAvail();
this.onRangingSuspended(_arg0, _arg1);
break;
}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
index e9f53ea..d19d1eb 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -187,7 +187,6 @@
_arg0 = data.readTypedObject(androidx.core.uwb.backend.RangingParameters.CREATOR);
androidx.core.uwb.backend.IRangingSessionCallback _arg1;
_arg1 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
- data.enforceNoDataAvail();
this.startRanging(_arg0, _arg1);
reply.writeNoException();
break;
@@ -196,7 +195,6 @@
{
androidx.core.uwb.backend.IRangingSessionCallback _arg0;
_arg0 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
- data.enforceNoDataAvail();
this.stopRanging(_arg0);
reply.writeNoException();
break;
@@ -205,7 +203,6 @@
{
androidx.core.uwb.backend.UwbAddress _arg0;
_arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
- data.enforceNoDataAvail();
this.addControlee(_arg0);
reply.writeNoException();
break;
@@ -214,7 +211,6 @@
{
androidx.core.uwb.backend.UwbAddress _arg0;
_arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
- data.enforceNoDataAvail();
this.removeControlee(_arg0);
reply.writeNoException();
break;
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
index fd59879..3b04bca8 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
@@ -28,12 +28,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.credentials.Credential;
import androidx.credentials.CredentialManagerCallback;
import androidx.credentials.GetCredentialRequest;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.GetPasswordOption;
-import androidx.credentials.PasswordCredential;
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController;
import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController;
@@ -42,7 +40,6 @@
import androidx.test.filters.SmallTest;
import com.google.android.gms.auth.api.identity.BeginSignInRequest;
-import com.google.android.gms.auth.api.identity.SignInCredential;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -143,9 +140,9 @@
public void convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
ActivityScenario<TestCredentialsActivity> activityScenario =
ActivityScenario.launch(TestCredentialsActivity.class);
- String expectedId = "id";
- String expectedPassword = "password";
- String expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL;
+ // TODO add back String expectedId = "id";
+ // TODO add back String expectedPassword = "password";
+ // TODO add back String expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL;
activityScenario.onActivity(activity -> {
CredentialProviderBeginSignInController beginSignInController =
CredentialProviderBeginSignInController
@@ -163,18 +160,20 @@
};
beginSignInController.executor = Runnable::run;
+ /** TODO figure out how to test given updated changes
Credential actualResponse =
beginSignInController
.convertResponseToCredentialManager(
new SignInCredential(expectedId, null, null,
null, null, expectedPassword,
- null, null)
+ null, null, null)
).getCredential();
assertThat(actualResponse.getType()).isEqualTo(expectedType);
assertThat(((PasswordCredential) actualResponse).getPassword())
.isEqualTo(expectedPassword);
assertThat(((PasswordCredential) actualResponse).getId()).isEqualTo(expectedId);
+ */
});
}
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
index a951af4..4f5a8ce 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
@@ -22,7 +22,6 @@
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
-import androidx.credentials.PasswordCredential
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.playservices.TestUtils.Companion.clearFragmentManager
import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
@@ -30,7 +29,6 @@
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.google.android.gms.auth.api.identity.SignInCredential
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import org.junit.Test
@@ -152,9 +150,9 @@
val activityScenario = ActivityScenario.launch(
TestCredentialsActivity::class.java
)
- val expectedId = "id"
- val expectedPassword = "password"
- val expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
+ // TODO add back val expectedId = "id"
+ // TODO add back val expectedPassword = "password"
+ // TODO add back val expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
activityScenario.onActivity { activity: TestCredentialsActivity ->
val beginSignInController =
CredentialProviderBeginSignInController
@@ -168,12 +166,14 @@
beginSignInController.executor =
Executor { obj: Runnable -> obj.run() }
+ /**
+ * TODO uncomment once SignInCredential testable solution found outside of Auth 20.3.0
val actualResponse = beginSignInController
.convertResponseToCredentialManager(
SignInCredential(
expectedId, null, null,
null, null, expectedPassword,
- null, null
+ null, null, null
)
).credential
@@ -182,6 +182,7 @@
.isEqualTo(expectedPassword)
assertThat(actualResponse.id)
.isEqualTo(expectedId)
+ */
}
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index a1fecef..cbc3713 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -34,6 +34,7 @@
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
+import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController
import java.util.concurrent.Executor
/**
@@ -93,7 +94,11 @@
callback,
executor)
} else if (request is CreatePublicKeyCredentialRequest) {
- // TODO("Add in")
+ CredentialProviderCreatePublicKeyCredentialController.getInstance(
+ fragmentManager).invokePlayServices(
+ request,
+ callback,
+ executor)
} else {
throw UnsupportedOperationException(
"Unsupported request; not password or publickeycredential")
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
index 83f8738..5c242de 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
@@ -32,8 +32,11 @@
* limitations under the License.
*/
+import android.util.Log
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPasswordOption
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.PublicKeyCredentialControllerUtility.Companion.convertToPlayAuthPasskeyRequest
import com.google.android.gms.auth.api.identity.BeginSignInRequest
/**
@@ -44,8 +47,11 @@
class BeginSignInControllerUtility {
companion object {
+
+ private val TAG = BeginSignInControllerUtility::class.java.name
internal fun constructBeginSignInRequest(request: GetCredentialRequest):
BeginSignInRequest {
+ var isPublicKeyCredReqFound = false
val requestBuilder = BeginSignInRequest.Builder()
for (option in request.getCredentialOptions) {
if (option is GetPasswordOption) {
@@ -54,8 +60,15 @@
.setSupported(true)
.build()
)
+ } else if (option is GetPublicKeyCredentialOption && !isPublicKeyCredReqFound) {
+ Log.i(TAG, "See request for passkey $option")
+ requestBuilder.setPasskeysSignInRequestOptions(
+ convertToPlayAuthPasskeyRequest(option)
+ )
+ isPublicKeyCredReqFound = true
+ // TODO("Confirm logic for single vs multiple options of the same type")
}
- // TODO("Add GoogleIDToken version and passkey version")
+ // TODO("Add GoogleIDToken version")
}
return requestBuilder
.setAutoSelectEnabled(request.isAutoSelectAllowed)
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
index 21e974f..7307adf 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -28,7 +28,7 @@
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
-import androidx.credentials.exceptions.GetCredentialCanceledException
+import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialUnknownException
@@ -132,7 +132,7 @@
if (resultCode != Activity.RESULT_OK) {
var exception: GetCredentialException = GetCredentialUnknownException()
if (resultCode == Activity.RESULT_CANCELED) {
- exception = GetCredentialCanceledException()
+ exception = GetCredentialCancellationException()
}
this.executor.execute { -> this.callback.onError(exception) }
return
@@ -149,7 +149,7 @@
var exception: GetCredentialException = GetCredentialUnknownException()
if (e.statusCode == CommonStatusCodes.CANCELED) {
Log.i(TAG, "User cancelled the prompt!")
- exception = GetCredentialCanceledException()
+ exception = GetCredentialCancellationException()
} else if (e.statusCode in this.retryables) {
exception = GetCredentialInterruptedException()
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
index 19ff3cf..1232a89 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -27,7 +27,7 @@
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.CredentialManagerCallback
-import androidx.credentials.exceptions.CreateCredentialCanceledException
+import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.CreateCredentialUnknownException
@@ -126,7 +126,7 @@
if (resultCode != Activity.RESULT_OK) {
var exception: CreateCredentialException = CreateCredentialUnknownException()
if (resultCode == Activity.RESULT_CANCELED) {
- exception = CreateCredentialCanceledException()
+ exception = CreateCredentialCancellationException()
}
this.executor.execute { -> this.callback.onError(exception) }
return
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
index 204b12f..b8ecc6e 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -16,13 +16,25 @@
package androidx.credentials.playservices.controllers.CreatePublicKeyCredential
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.PendingIntent
import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
import android.util.Log
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.fido.Fido
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
import java.util.concurrent.Executor
@@ -52,12 +64,59 @@
*/
private lateinit var executor: Executor
+ @SuppressLint("ClassVerificationFailure")
override fun invokePlayServices(
request: CreatePublicKeyCredentialRequest,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
executor: Executor
) {
- TODO("Not yet implemented")
+ this.callback = callback
+ this.executor = executor
+ val fidoRegistrationRequest: PublicKeyCredentialCreationOptions =
+ this.convertRequestToPlayServices(request)
+ Fido.getFido2ApiClient(getActivity())
+ .getRegisterPendingIntent(fidoRegistrationRequest)
+ .addOnSuccessListener { result: PendingIntent ->
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ startIntentSenderForResult(
+ result.intentSender,
+ REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL,
+ null, /* fillInIntent= */
+ 0, /* flagsMask= */
+ 0, /* flagsValue= */
+ 0, /* extraFlags= */
+ null /* options= */
+ )
+ }
+ } catch (e: IntentSender.SendIntentException) {
+ Log.i(
+ TAG,
+ "Failed to send pending intent for fido client " +
+ " : " + e.message
+ )
+ val exception: CreatePublicKeyCredentialException =
+ CreatePublicKeyCredentialUnknownException()
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ }
+ }
+ .addOnFailureListener { e: Exception ->
+ var exception: CreatePublicKeyCredentialException =
+ CreatePublicKeyCredentialUnknownException()
+ if (e is ApiException && e.statusCode in this.retryables) {
+ exception = CreatePublicKeyCredentialInterruptedException()
+ }
+ Log.i(TAG, "Fido Registration failed with error: " + e.message)
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -67,22 +126,49 @@
private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
Log.i(TAG, "$uniqueRequestCode $resultCode $data")
- TODO("Not yet implemented")
+ if (uniqueRequestCode != REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL) {
+ return
+ }
+ if (resultCode != Activity.RESULT_OK) {
+ var exception: CreateCredentialException =
+ CreatePublicKeyCredentialUnknownException()
+ if (resultCode == Activity.RESULT_CANCELED) {
+ exception = CreateCredentialCancellationException()
+ }
+ this.executor.execute { -> this.callback.onError(exception) }
+ return
+ }
+ val bytes: ByteArray? = data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
+ if (bytes == null) {
+ this.executor.execute { this.callback.onError(
+ CreatePublicKeyCredentialUnknownException(
+ "Internal error fido module giving null bytes")
+ ) }
+ return
+ }
+ val cred: PublicKeyCredential = PublicKeyCredential.deserializeFromBytes(bytes)
+ if (PublicKeyCredentialControllerUtility.publicKeyCredentialResponseContainsError(
+ this.callback, this.executor, cred)) {
+ return
+ }
+ val response = this.convertResponseToCredentialManager(cred)
+ this.executor.execute { this.callback.onResult(response) }
}
override fun convertRequestToPlayServices(request: CreatePublicKeyCredentialRequest):
PublicKeyCredentialCreationOptions {
- TODO("Not yet implemented")
+ return PublicKeyCredentialControllerUtility.convert(request)
}
override fun convertResponseToCredentialManager(response: PublicKeyCredential):
CreateCredentialResponse {
- TODO("Not yet implemented")
+ return CreatePublicKeyCredentialResponse(PublicKeyCredentialControllerUtility
+ .toCreatePasskeyResponseJson(response))
}
companion object {
private val TAG = CredentialProviderCreatePublicKeyCredentialController::class.java.name
- private const val REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL: Int = 1
+ private const val REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL: Int = 1
// TODO("Ensure this works with the lifecycle")
/**
@@ -97,12 +183,12 @@
fun getInstance(fragmentManager: android.app.FragmentManager):
CredentialProviderCreatePublicKeyCredentialController {
var controller = findPastController(
- REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL,
+ REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL,
fragmentManager)
if (controller == null) {
controller = CredentialProviderCreatePublicKeyCredentialController()
fragmentManager.beginTransaction().add(controller,
- REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL.toString())
+ REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL.toString())
.commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
new file mode 100644
index 0000000..dd380f0
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -0,0 +1,266 @@
+/*
+ * 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.credentials.playservices.controllers.CreatePublicKeyCredential
+
+import android.util.Base64
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.SignInCredential
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria
+import com.google.android.gms.fido.fido2.api.common.ErrorCode
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
+import java.util.concurrent.Executor
+import org.json.JSONArray
+import org.json.JSONObject
+
+/**
+ * A utility class to handle logic for the begin sign in controller.
+ *
+ * @hide
+ */
+class PublicKeyCredentialControllerUtility {
+
+ companion object {
+ @JvmStatic
+ fun convert(request: CreatePublicKeyCredentialRequest): PublicKeyCredentialCreationOptions {
+ val requestJson = request.requestJson
+ val json = JSONObject(requestJson)
+ val builder: PublicKeyCredentialCreationOptions.Builder =
+ PublicKeyCredentialCreationOptions.Builder()
+
+ if (json.has("challenge")) {
+ Log.d(TAG, "Set challenge")
+ val challenge: ByteArray =
+ Base64.decode(json.getString("challenge"), Base64.URL_SAFE)
+ builder.setChallenge(challenge)
+ }
+
+ if (json.has("user")) {
+ Log.d(TAG, "Set user")
+ val user: JSONObject = json.getJSONObject("user")
+ val id: String = user.getString("id")
+ val name: String = user.getString("name")
+ val displayName: String = user.getString("displayName")
+ builder.setUser(
+ PublicKeyCredentialUserEntity(
+ id.toByteArray(),
+ name,
+ "",
+ displayName
+ )
+ )
+ }
+
+ if (json.has("rp")) {
+ Log.d(TAG, "Set rp")
+ val rp: JSONObject = json.getJSONObject("rp")
+ val id: String = rp.getString("id")
+ val name: String = rp.getString("name")
+ builder.setRp(
+ PublicKeyCredentialRpEntity(
+ id,
+ name,
+ null
+ )
+ )
+ }
+
+ if (json.has("timeout")) {
+ Log.d(TAG, "Set timeout")
+ val timeout: Double = json.getDouble("timeout") / 1000
+ builder.setTimeoutSeconds(timeout)
+ }
+
+ if (json.has("pubKeyCredParams")) {
+ Log.d(TAG, "Set pubKeyCredParams")
+ val pubKeyCredParams: JSONArray = json.getJSONArray("pubKeyCredParams")
+ val paramsList: MutableList<PublicKeyCredentialParameters> = ArrayList()
+ for (i in 0 until pubKeyCredParams.length()) {
+ val param = pubKeyCredParams.getJSONObject(i)
+ paramsList.add(
+ PublicKeyCredentialParameters(
+ param.getString("type"), param.getInt("alg"))
+ )
+ }
+ builder.setParameters(paramsList)
+ }
+
+ if (json.has("authenticatorSelection")) {
+ Log.d("PasskeyRequestConverter", "Set authenticatorSelection")
+ val authenticatorSelection: JSONObject = json.getJSONObject(
+ "authenticatorSelection")
+ val requireResidentKey = authenticatorSelection.getBoolean(
+ "requireResidentKey")
+ builder.setAuthenticatorSelection(
+ AuthenticatorSelectionCriteria.Builder()
+ .setRequireResidentKey(requireResidentKey).build()
+ )
+ }
+
+ builder.setExcludeList(ArrayList())
+
+ return builder.build()
+ }
+
+ fun toCreatePasskeyResponseJson(cred: PublicKeyCredential): String {
+ val json = JSONObject()
+
+ val authenticatorResponse: AuthenticatorResponse = cred.response
+ if (authenticatorResponse is AuthenticatorAttestationResponse) {
+ val responseJson = JSONObject()
+ responseJson.put(
+ "clientDataJSON",
+ Base64.encodeToString(authenticatorResponse.clientDataJSON, Base64.NO_WRAP))
+ responseJson.put(
+ "attestationObject",
+ Base64.encodeToString(authenticatorResponse.attestationObject, Base64.NO_WRAP))
+ val transports = JSONArray(listOf(authenticatorResponse.transports))
+ responseJson.put("transports", transports)
+ json.put("response", responseJson)
+ } else {
+ Log.e(
+ TAG,
+ "Expected registration response but got: " +
+ authenticatorResponse.javaClass.name)
+ }
+
+ if (cred.authenticatorAttachment != null) {
+ json.put("authenticatorAttachment", String(cred.rawId))
+ }
+
+ json.put("id", cred.id)
+ json.put("rawId", Base64.encodeToString(cred.rawId, Base64.NO_WRAP))
+ json.put("type", cred.type)
+ // TODO: add ExtensionsClientOUtputsJSON conversion
+ return json.toString()
+ }
+
+ fun toAssertPasskeyResponse(cred: SignInCredential): String {
+ val json = JSONObject()
+ val publicKeyCred = cred.publicKeyCredential
+ val authenticatorResponse: AuthenticatorResponse = publicKeyCred?.response!!
+
+ if (authenticatorResponse is AuthenticatorAssertionResponse) {
+ val responseJson = JSONObject()
+ responseJson.put(
+ "clientDataJSON",
+ Base64.encodeToString(authenticatorResponse.clientDataJSON, Base64.NO_WRAP))
+ responseJson.put(
+ "assertionObject",
+ Base64.encodeToString(authenticatorResponse.authenticatorData, Base64.NO_WRAP))
+ responseJson.put(
+ "signature",
+ Base64.encodeToString(authenticatorResponse.signature, Base64.NO_WRAP))
+ json.put("response", responseJson)
+ } else {
+ Log.e(
+ TAG,
+ "Expected assertion response but got: " + authenticatorResponse.javaClass.name)
+ }
+ json.put("id", publicKeyCred.id)
+ json.put("rawId", Base64.encodeToString(publicKeyCred.rawId, Base64.NO_WRAP))
+ json.put("type", publicKeyCred.type)
+ return json.toString()
+ }
+
+ @Suppress("DocumentExceptions")
+ fun convertToPlayAuthPasskeyRequest(request: GetPublicKeyCredentialOption):
+ BeginSignInRequest.PasskeysRequestOptions {
+ // TODO : Make sure this is in compliance with w3
+ val json = JSONObject(request.requestJson)
+ if (json.has("rpId")) {
+ val rpId: String = json.getString("rpId")
+ Log.i(TAG, "Rp Id : $rpId")
+ if (json.has("challenge")) {
+ val challenge: ByteArray =
+ Base64.decode(json.getString("challenge"), Base64.URL_SAFE)
+ return BeginSignInRequest.PasskeysRequestOptions.Builder()
+ .setSupported(true)
+ .setRpId(rpId)
+ .setChallenge(challenge)
+ .build()
+ } else {
+ Log.i(TAG, "Challenge not found in request for : $rpId")
+ }
+ } else {
+ Log.i(TAG, "Rp Id not found in request")
+ }
+ throw UnsupportedOperationException("rpId not specified in the request")
+ }
+
+ /**
+ * Indicates if an error was propagated from the underlying Fido API.
+ *
+ * @param callback the callback invoked when the request succeeds or fails
+ * @param executor the callback will take place on this executor
+ * @param cred the public key credential response object from fido
+ *
+ * @return true if there is an error, false otherwise
+ */
+ fun publicKeyCredentialResponseContainsError(
+ callback: CredentialManagerCallback<CreateCredentialResponse,
+ CreateCredentialException>,
+ executor: Executor,
+ cred: PublicKeyCredential
+ ): Boolean {
+ val authenticatorResponse: AuthenticatorResponse = cred.response
+ if (authenticatorResponse is AuthenticatorErrorResponse) {
+ val code = authenticatorResponse.errorCode
+ var exception = orderedErrorCodeToExceptions[code]
+ if (exception == null) {
+ exception = CreatePublicKeyCredentialUnknownException("unknown fido gms " +
+ "exception")
+ }
+ executor.execute { callback.onError(
+ exception
+ ) }
+ return true
+ }
+ return false
+ }
+
+ private val TAG = PublicKeyCredentialControllerUtility::class.java.name
+
+ internal val orderedErrorCodeToExceptions = linkedMapOf(ErrorCode.UNKNOWN_ERR to
+ CreatePublicKeyCredentialUnknownException("fido gms returned unknown transient failure"),
+ ErrorCode.ABORT_ERR to CreatePublicKeyCredentialAbortException("fido gms indicates the " +
+ "operation was aborted"),
+ ErrorCode.CONSTRAINT_ERR to CreatePublicKeyCredentialConstraintException("fido gms " +
+ "indicates a constraint was not satisfied due to some mutation operation"),
+ ErrorCode.ATTESTATION_NOT_PRIVATE_ERR to
+ CreatePublicKeyCredentialNotReadableException("fido gms indicates the " +
+ "authenticator violates privacy requirements")
+ )
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -183,9 +183,9 @@
ctor public ClearCredentialUnknownException();
}
- public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public CreateCredentialCanceledException();
+ public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+ ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public CreateCredentialCancellationException();
}
public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
ctor public CreateCredentialUnknownException();
}
- public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public GetCredentialCanceledException();
+ public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+ ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public GetCredentialCancellationException();
}
public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -183,9 +183,9 @@
ctor public ClearCredentialUnknownException();
}
- public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public CreateCredentialCanceledException();
+ public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+ ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public CreateCredentialCancellationException();
}
public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
ctor public CreateCredentialUnknownException();
}
- public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public GetCredentialCanceledException();
+ public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+ ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public GetCredentialCancellationException();
}
public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -183,9 +183,9 @@
ctor public ClearCredentialUnknownException();
}
- public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
- ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public CreateCredentialCanceledException();
+ public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+ ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public CreateCredentialCancellationException();
}
public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
ctor public CreateCredentialUnknownException();
}
- public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
- ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
- ctor public GetCredentialCanceledException();
+ public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+ ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+ ctor public GetCredentialCancellationException();
}
public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
deleted file mode 100644
index 903bc23..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
+++ /dev/null
@@ -1,45 +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.credentials.exceptions
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class CreateCredentialCanceledExceptionTest {
- @Test(expected = CreateCredentialCanceledException::class)
- fun construct_inputNonEmpty_success() {
- throw CreateCredentialCanceledException("msg")
- }
-
- @Test(expected = CreateCredentialCanceledException::class)
- fun construct_errorMessageNull_success() {
- throw CreateCredentialCanceledException(null)
- }
-
- @Test
- fun getter_type_success() {
- val exception = CreateCredentialCanceledException("msg")
- val expectedType = CreateCredentialCanceledException
- .TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION
- Truth.assertThat(exception.type).isEqualTo(expectedType)
- }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
similarity index 66%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
index 9dc4526..2b11f40 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
@@ -26,22 +26,23 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class CreateCredentialCanceledExceptionJavaTest {
- @Test(expected = CreateCredentialCanceledException.class)
- public void construct_inputNonEmpty_success() throws CreateCredentialCanceledException {
- throw new CreateCredentialCanceledException("msg");
+public class CreateCredentialCancellationExceptionJavaTest {
+ @Test(expected = CreateCredentialCancellationException.class)
+ public void construct_inputNonEmpty_success() throws CreateCredentialCancellationException {
+ throw new CreateCredentialCancellationException("msg");
}
- @Test(expected = CreateCredentialCanceledException.class)
- public void construct_errorMessageNull_success() throws CreateCredentialCanceledException {
- throw new CreateCredentialCanceledException(null);
+ @Test(expected = CreateCredentialCancellationException.class)
+ public void construct_errorMessageNull_success() throws CreateCredentialCancellationException {
+ throw new CreateCredentialCancellationException(null);
}
@Test
public void getter_type_success() {
- CreateCredentialCanceledException exception = new CreateCredentialCanceledException("msg");
+ CreateCredentialCancellationException exception = new
+ CreateCredentialCancellationException("msg");
String expectedType =
- CreateCredentialCanceledException.TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION;
+ CreateCredentialCancellationException.TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION;
Truth.assertThat(exception.getType()).isEqualTo(expectedType);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
similarity index 69%
copy from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
index 61e7c3c..3ff4aa3 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
@@ -24,21 +24,22 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class GetCredentialCanceledExceptionTest {
- @Test(expected = GetCredentialCanceledException::class)
+class CreateCredentialCancellationExceptionTest {
+ @Test(expected = CreateCredentialCancellationException::class)
fun construct_inputNonEmpty_success() {
- throw GetCredentialCanceledException("msg")
+ throw CreateCredentialCancellationException("msg")
}
- @Test(expected = GetCredentialCanceledException::class)
+ @Test(expected = CreateCredentialCancellationException::class)
fun construct_errorMessageNull_success() {
- throw GetCredentialCanceledException(null)
+ throw CreateCredentialCancellationException(null)
}
@Test
fun getter_type_success() {
- val exception = GetCredentialCanceledException("msg")
- val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+ val exception = CreateCredentialCancellationException("msg")
+ val expectedType = CreateCredentialCancellationException
+ .TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION
Truth.assertThat(exception.type).isEqualTo(expectedType)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
similarity index 65%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
index e5cf2f3..64653dc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
@@ -26,21 +26,23 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class GetCredentialCanceledExceptionJavaTest {
- @Test(expected = GetCredentialCanceledException.class)
- public void construct_inputNonEmpty_success() throws GetCredentialCanceledException {
- throw new GetCredentialCanceledException("msg");
+public class GetCredentialCancellationExceptionJavaTest {
+ @Test(expected = GetCredentialCancellationException.class)
+ public void construct_inputNonEmpty_success() throws GetCredentialCancellationException {
+ throw new GetCredentialCancellationException("msg");
}
- @Test(expected = GetCredentialCanceledException.class)
- public void construct_errorMessageNull_success() throws GetCredentialCanceledException {
- throw new GetCredentialCanceledException(null);
+ @Test(expected = GetCredentialCancellationException.class)
+ public void construct_errorMessageNull_success() throws GetCredentialCancellationException {
+ throw new GetCredentialCancellationException(null);
}
@Test
public void getter_type_success() {
- GetCredentialCanceledException exception = new GetCredentialCanceledException("msg");
- String expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION;
+ GetCredentialCancellationException exception = new
+ GetCredentialCancellationException("msg");
+ String expectedType = GetCredentialCancellationException
+ .TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION;
Truth.assertThat(exception.getType()).isEqualTo(expectedType);
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
similarity index 70%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
index 61e7c3c..bbc2d3e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
@@ -24,21 +24,22 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class GetCredentialCanceledExceptionTest {
- @Test(expected = GetCredentialCanceledException::class)
+class GetCredentialCancellationExceptionTest {
+ @Test(expected = GetCredentialCancellationException::class)
fun construct_inputNonEmpty_success() {
- throw GetCredentialCanceledException("msg")
+ throw GetCredentialCancellationException("msg")
}
- @Test(expected = GetCredentialCanceledException::class)
+ @Test(expected = GetCredentialCancellationException::class)
fun construct_errorMessageNull_success() {
- throw GetCredentialCanceledException(null)
+ throw GetCredentialCancellationException(null)
}
@Test
fun getter_type_success() {
- val exception = GetCredentialCanceledException("msg")
- val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+ val exception = GetCredentialCancellationException("msg")
+ val expectedType = GetCredentialCancellationException
+ .TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION
Truth.assertThat(exception.type).isEqualTo(expectedType)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java
new file mode 100644
index 0000000..5978e95
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialAbortExceptionJavaTest {
+
+ @Test(expected = CreatePublicKeyCredentialAbortException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreatePublicKeyCredentialAbortException {
+ throw new CreatePublicKeyCredentialAbortException(
+ "msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialAbortException.class)
+ public void construct_errorMessageNull_success() throws
+ CreatePublicKeyCredentialAbortException {
+ throw new CreatePublicKeyCredentialAbortException(null);
+ }
+
+ @Test
+ public void getter_type_success() {
+ CreatePublicKeyCredentialAbortException exception = new
+ CreatePublicKeyCredentialAbortException("msg");
+ String expectedType =
+ CreatePublicKeyCredentialAbortException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION;
+ Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
similarity index 60%
copy from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
index 61e7c3c..123dc94 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.credentials.exceptions
+package androidx.credentials.exceptions.createpublickeycredential
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
@@ -24,21 +25,24 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class GetCredentialCanceledExceptionTest {
- @Test(expected = GetCredentialCanceledException::class)
+class CreatePublicKeyCredentialAbortExceptionTest {
+
+ @Test(expected = CreatePublicKeyCredentialAbortException::class)
fun construct_inputNonEmpty_success() {
- throw GetCredentialCanceledException("msg")
+ throw CreatePublicKeyCredentialAbortException("msg")
}
- @Test(expected = GetCredentialCanceledException::class)
+ @Test(expected = CreatePublicKeyCredentialAbortException::class)
fun construct_errorMessageNull_success() {
- throw GetCredentialCanceledException(null)
+ throw CreatePublicKeyCredentialAbortException(null)
}
@Test
fun getter_type_success() {
- val exception = GetCredentialCanceledException("msg")
- val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+ val exception = CreatePublicKeyCredentialAbortException("msg")
+ val expectedType =
+ CreatePublicKeyCredentialAbortException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION
Truth.assertThat(exception.type).isEqualTo(expectedType)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java
new file mode 100644
index 0000000..cde3616
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialConstraintExceptionJavaTest {
+ @Test(expected = CreatePublicKeyCredentialConstraintException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreatePublicKeyCredentialConstraintException {
+ throw new CreatePublicKeyCredentialConstraintException("msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialConstraintException.class)
+ public void construct_errorMessageNull_success() throws
+ CreatePublicKeyCredentialConstraintException {
+ throw new CreatePublicKeyCredentialConstraintException(null);
+ }
+
+ @Test
+ public void getter_type_success() {
+ CreatePublicKeyCredentialConstraintException exception = new
+ CreatePublicKeyCredentialConstraintException("msg");
+ String expectedType =
+ CreatePublicKeyCredentialConstraintException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION;
+ Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt
new file mode 100644
index 0000000..1dcfbeb
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialConstraintExceptionTest {
+ @Test(expected = CreatePublicKeyCredentialConstraintException::class)
+ fun construct_inputNonEmpty_success() {
+ throw CreatePublicKeyCredentialConstraintException("msg")
+ }
+
+ @Test(expected = CreatePublicKeyCredentialConstraintException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreatePublicKeyCredentialConstraintException(null)
+ }
+
+ @Test
+ fun getter_type_success() {
+ val exception = CreatePublicKeyCredentialConstraintException("msg")
+ val expectedType =
+ CreatePublicKeyCredentialConstraintException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION
+ Truth.assertThat(exception.type).isEqualTo(expectedType)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java
new file mode 100644
index 0000000..508f137
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialExceptionJavaTest {
+
+ @Test(expected = CreatePublicKeyCredentialException.class)
+ public void construct_inputsNonEmpty_success() throws CreatePublicKeyCredentialException {
+ throw new CreatePublicKeyCredentialException("type", "msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialException.class)
+ public void construct_errorMessageNull_success() throws CreatePublicKeyCredentialException {
+ throw new CreatePublicKeyCredentialException("type", null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void construct_typeEmpty_throws() throws CreatePublicKeyCredentialException {
+ throw new CreatePublicKeyCredentialException("", "msg");
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void construct_typeNull_throws() throws CreatePublicKeyCredentialException {
+ throw new CreatePublicKeyCredentialException(null, "msg");
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt
new file mode 100644
index 0000000..eeeb47a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialExceptionTest {
+
+ @Test(expected = CreatePublicKeyCredentialException::class)
+ fun construct_inputsNonEmpty_success() {
+ throw CreatePublicKeyCredentialException("type", "msg")
+ }
+
+ @Test(expected = CreatePublicKeyCredentialException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreatePublicKeyCredentialException("type", null)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun construct_typeEmpty_throws() {
+ throw CreatePublicKeyCredentialException("", "msg")
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java
new file mode 100644
index 0000000..89464e7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialInterruptedExceptionJavaTest {
+ @Test(expected = CreatePublicKeyCredentialInterruptedException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreatePublicKeyCredentialInterruptedException {
+ throw new CreatePublicKeyCredentialInterruptedException("msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialInterruptedException.class)
+ public void construct_errorMessageNull_success() throws
+ CreatePublicKeyCredentialInterruptedException {
+ throw new CreatePublicKeyCredentialInterruptedException(null);
+ }
+
+ @Test
+ public void getter_type_success() {
+ CreatePublicKeyCredentialInterruptedException exception = new
+ CreatePublicKeyCredentialInterruptedException("msg");
+ String expectedType =
+ CreatePublicKeyCredentialInterruptedException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION;
+ Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt
new file mode 100644
index 0000000..0d73064
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialInterruptedExceptionTest {
+ @Test(expected = CreatePublicKeyCredentialInterruptedException::class)
+ fun construct_inputNonEmpty_success() {
+ throw CreatePublicKeyCredentialInterruptedException("msg")
+ }
+
+ @Test(expected = CreatePublicKeyCredentialInterruptedException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreatePublicKeyCredentialInterruptedException(null)
+ }
+
+ @Test
+ fun getter_type_success() {
+ val exception = CreatePublicKeyCredentialInterruptedException("msg")
+ val expectedType =
+ CreatePublicKeyCredentialInterruptedException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION
+ Truth.assertThat(exception.type).isEqualTo(expectedType)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java
new file mode 100644
index 0000000..c28a1c6
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialNotReadableExceptionJavaTest {
+ @Test(expected = CreatePublicKeyCredentialNotReadableException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreatePublicKeyCredentialNotReadableException {
+ throw new CreatePublicKeyCredentialNotReadableException("msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialNotReadableException.class)
+ public void construct_errorMessageNull_success() throws
+ CreatePublicKeyCredentialNotReadableException {
+ throw new CreatePublicKeyCredentialNotReadableException(null);
+ }
+
+ @Test
+ public void getter_type_success() {
+ CreatePublicKeyCredentialNotReadableException exception = new
+ CreatePublicKeyCredentialNotReadableException("msg");
+ String expectedType =
+ CreatePublicKeyCredentialNotReadableException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION;
+ Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt
new file mode 100644
index 0000000..e84984a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialNotReadableExceptionTest {
+ @Test(expected = CreatePublicKeyCredentialNotReadableException::class)
+ fun construct_inputNonEmpty_success() {
+ throw CreatePublicKeyCredentialNotReadableException("msg")
+ }
+
+ @Test(expected = CreatePublicKeyCredentialNotReadableException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreatePublicKeyCredentialNotReadableException(null)
+ }
+
+ @Test
+ fun getter_type_success() {
+ val exception = CreatePublicKeyCredentialNotReadableException("msg")
+ val expectedType =
+ CreatePublicKeyCredentialNotReadableException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION
+ Truth.assertThat(exception.type).isEqualTo(expectedType)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java
new file mode 100644
index 0000000..bf3c6bb
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialUnknownExceptionJavaTest {
+ @Test(expected = CreatePublicKeyCredentialUnknownException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreatePublicKeyCredentialUnknownException {
+ throw new CreatePublicKeyCredentialUnknownException("msg");
+ }
+
+ @Test(expected = CreatePublicKeyCredentialUnknownException.class)
+ public void construct_errorMessageNull_success() throws
+ CreatePublicKeyCredentialUnknownException {
+ throw new CreatePublicKeyCredentialUnknownException(null);
+ }
+
+ @Test
+ public void getter_type_success() {
+ CreatePublicKeyCredentialUnknownException exception = new
+ CreatePublicKeyCredentialUnknownException("msg");
+ String expectedType =
+ CreatePublicKeyCredentialUnknownException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION;
+ Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt
new file mode 100644
index 0000000..c1bf411
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialUnknownExceptionTest {
+ @Test(expected = CreatePublicKeyCredentialUnknownException::class)
+ fun construct_inputNonEmpty_success() {
+ throw CreatePublicKeyCredentialUnknownException("msg")
+ }
+
+ @Test(expected = CreatePublicKeyCredentialUnknownException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreatePublicKeyCredentialUnknownException(null)
+ }
+
+ @Test
+ fun getter_type_success() {
+ val exception = CreatePublicKeyCredentialUnknownException("msg")
+ val expectedType =
+ CreatePublicKeyCredentialUnknownException
+ .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION
+ Truth.assertThat(exception.type).isEqualTo(expectedType)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
index a9793d2..efc15ac 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
import androidx.annotation.VisibleForTesting
/**
- * During the clear credential flow, this is called when some interruption occurs that may warrant
+ * During the clear credential flow, this is returned when some interruption occurs that may warrant
* retrying or at least does not indicate a purposeful desire to close or tap away from credential
* manager.
*
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
similarity index 71%
rename from credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt
rename to credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
index 2c205c4..0be4594 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
@@ -19,20 +19,20 @@
import androidx.annotation.VisibleForTesting
/**
- * During the create credential flow, this is called when a user intentionally cancels an operation.
+ * During the create credential flow, this is returned when a user intentionally cancels an operation.
* When this happens, the application should handle logic accordingly, typically under indication
* the user does not want to see Credential Manager anymore.
*
* @see CreateCredentialException
*/
-class CreateCredentialCanceledException @JvmOverloads constructor(
+class CreateCredentialCancellationException @JvmOverloads constructor(
errorMessage: CharSequence? = null
-) : CreateCredentialException(TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION, errorMessage) {
+) : CreateCredentialException(TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION, errorMessage) {
/** @hide */
companion object {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- const val TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION: String =
- "androidx.credentials.TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION"
+ const val TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION"
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
index ec8c14b..b2ba63c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
@@ -25,7 +25,7 @@
*
* @see CredentialManager
* @see CreateCredentialInterruptedException
- * @see CreateCredentialCanceledException
+ * @see CreateCredentialCancellationException
* @see CreateCredentialUnknownException
*
* @property errorMessage a human-readable string that describes the error
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
index 9dbaacb..ec2c746 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
import androidx.annotation.VisibleForTesting
/**
- * During the create credential flow, this is called when some interruption occurs that may warrant
+ * During the create credential flow, this is returned when some interruption occurs that may warrant
* retrying or at least does not indicate a purposeful desire to close or tap away from credential
* manager.
*
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
similarity index 72%
rename from credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt
rename to credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
index 5ed263f..63e5e25 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
@@ -19,20 +19,20 @@
import androidx.annotation.VisibleForTesting
/**
- * During the get credential flow, this is called when a user intentionally cancels an operation.
+ * During the get credential flow, this is returned when a user intentionally cancels an operation.
* When this happens, the application should handle logic accordingly, typically under indication
* the user does not want to see Credential Manager anymore.
*
* @see GetCredentialException
*/
-class GetCredentialCanceledException @JvmOverloads constructor(
+class GetCredentialCancellationException @JvmOverloads constructor(
errorMessage: CharSequence? = null
-) : GetCredentialException(TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION, errorMessage) {
+) : GetCredentialException(TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION, errorMessage) {
/** @hide */
companion object {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- const val TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION: String =
- "androidx.credentials.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION"
+ const val TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION: String =
+ "androidx.credentials.TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION"
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
index 372584c..752252d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
@@ -25,7 +25,7 @@
*
* @see CredentialManager
* @see GetCredentialUnknownException
- * @see GetCredentialCanceledException
+ * @see GetCredentialCancellationException
* @see GetCredentialInterruptedException
*
* @property errorMessage a human-readable string that describes the error
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
index f352c65..ae9c993 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
import androidx.annotation.VisibleForTesting
/**
- * During the get credential flow, this is called when some interruption occurs that may warrant
+ * During the get credential flow, this is returned when some interruption occurs that may warrant
* retrying or at least does not indicate a purposeful desire to close or tap away from credential
* manager.
*
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt
new file mode 100644
index 0000000..4681796
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains and abort-err from the fido spec, indicating the operation was aborted. The
+ * fido spec can be found [here](https://webidl.spec.whatwg.org/#idl-DOMException).
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialAbortException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+ TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION,
+ errorMessage) {
+ /** @hide */
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+ "_ABORT_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt
new file mode 100644
index 0000000..412617f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains a constraint_err code, indicating that some mutation operation
+ * occurring during a transaction failed by not satisfying constraints.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialConstraintException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+ TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION,
+ errorMessage) {
+
+ /** @hide */
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+ "_CONSTRAINT_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt
new file mode 100644
index 0000000..b5b59c6
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.credentials.CredentialManager
+import androidx.credentials.exceptions.CreateCredentialException
+
+/**
+ * A subclass of CreateCredentialException for unique errors specific only to PublicKeyCredentials.
+ * See [CredentialManager] for more details on how Credentials work for Credential Manager flows.
+ * See [GMS Error Codes](https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/ErrorCode)
+ * for more details on some of the subclasses.
+ *
+ * @see CredentialManager
+ * @see CreatePublicKeyCredentialInterruptedException
+ * @see CreatePublicKeyCredentialUnknownException
+ * @see CreatePublicKeyCredentialNotReadableException
+ * @see CreatePublicKeyCredentialAbortException
+ * @see CreatePublicKeyCredentialConstraintException
+ *
+ * @property errorMessage a human-readable string that describes the error
+ * @throws NullPointerException if [type] is null
+ * @throws IllegalArgumentException if [type] is empty
+ *
+ * @hide
+ */
+open class CreatePublicKeyCredentialException @JvmOverloads constructor(
+ type: String,
+ errorMessage: CharSequence? = null
+) : CreateCredentialException(type, errorMessage)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt
new file mode 100644
index 0000000..395183c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential credential flow, this is returned when some interruption
+ * occurs that may warrant retrying or at least does not indicate a purposeful desire to close or
+ * tap away from credential manager.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialInterruptedException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+ TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION,
+ errorMessage) {
+
+ /** @hide */
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt
new file mode 100644
index 0000000..afd339f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains a NotReadableError from fido, which indicates there was some I/O read
+ * operation that failed.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialNotReadableException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+ TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION,
+ errorMessage) {
+ /** @hide */
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+ "_NOT_READABLE_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt
new file mode 100644
index 0000000..ed9538c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * This create public key credential operation failed with no more detailed information. This could
+ * be something such as out of memory or some other transient reason.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialUnknownException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+ TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION,
+ errorMessage) {
+
+ /** @hide */
+ companion object {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION: String =
+ "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 5d5e448..5c5329b 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -307,6 +307,11 @@
docs(project(":versionedparcelable:versionedparcelable"))
docs(project(":viewpager2:viewpager2"))
docs(project(":viewpager:viewpager"))
+ docs(project(":wear:protolayout:protolayout"))
+ docs(project(":wear:protolayout:protolayout-expression"))
+ docs(project(":wear:protolayout:protolayout-expression-pipeline"))
+ docs(project(":wear:protolayout:protolayout-proto"))
+ docs(project(":wear:protolayout:protolayout-renderer"))
docs(project(":wear:wear"))
stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
docs(project(":wear:compose:compose-foundation"))
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
new file mode 100644
index 0000000..e4ee5f4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
@@ -0,0 +1,28 @@
+<?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.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.emoji2.emojipicker.EmojiPickerView
+ android:id="@+id/emoji_picker"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:emojiGridRows="6"
+ app:emojiGridColumns="20" />
+</FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
index 3b15586..b31c40e 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
@@ -14,13 +14,15 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="match_parent">
<androidx.emoji2.emojipicker.EmojiPickerView
android:id="@+id/emoji_picker"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
-</LinearLayout>
+ android:layout_height="match_parent"
+ app:emojiGridRows="15"
+ app:emojiGridColumns="9" />
+</FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
index b9b80a5..af0e9a2 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -17,14 +17,11 @@
package androidx.emoji2.emojipicker
import androidx.emoji2.emojipicker.R as EmojiPickerViewR
-import androidx.test.espresso.Espresso.onView
-import org.hamcrest.Description
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.View.GONE
-import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
@@ -32,18 +29,12 @@
import androidx.emoji2.emojipicker.test.R
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.action.ViewActions.longClick
-import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.BoundedMatcher
-import androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams
-import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
+import org.hamcrest.Description
import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -99,55 +90,6 @@
}
}
- @Test
- @SdkSuppress(minSdkVersion = 24)
- fun testCustomEmojiPickerView_hasVariant() {
- // 👃 has variants
- val noseEmoji = "\uD83D\uDC43"
- lateinit var bodyView: EmojiPickerBodyView
- mActivityTestRule.scenario.onActivity {
- bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
- .findViewById(EmojiPickerViewR.id.emoji_picker_body)
- }
- onView(withId(EmojiPickerViewR.id.emoji_picker_body))
- .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
- val targetView = findViewByEmoji(bodyView, noseEmoji)
- // Variant indicator visible
- assertEquals(
- (targetView.parent as FrameLayout).findViewById<ImageView>(
- EmojiPickerViewR.id.variant_availability_indicator
- ).visibility, VISIBLE
- )
- // Long-clickable
- assertEquals(targetView.isLongClickable, true)
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 24)
- fun testStickyVariant_displayAndSaved() {
- val noseEmoji = "\uD83D\uDC43"
- val noseEmojiDark = "\uD83D\uDC43\uD83C\uDFFF"
- lateinit var bodyView: EmojiPickerBodyView
- mActivityTestRule.scenario.onActivity {
- bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
- .findViewById(EmojiPickerViewR.id.emoji_picker_body)
- }
- // Scroll to the nose emoji, long click then select nose in dark skin tone
- onView(withId(EmojiPickerViewR.id.emoji_picker_body))
- .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
- onView(createEmojiViewMatcher(noseEmoji)).perform(longClick())
- onView(createEmojiViewMatcher(noseEmojiDark))
- .inRoot(hasWindowLayoutParams())
- .perform(click())
- assertNotNull(findViewByEmoji(bodyView, noseEmojiDark))
- // Switch back to clear saved preference
- onView(createEmojiViewMatcher(noseEmojiDark)).perform(longClick())
- onView(createEmojiViewMatcher(noseEmoji))
- .inRoot(hasWindowLayoutParams())
- .perform(click())
- assertNotNull(findViewByEmoji(bodyView, noseEmoji))
- }
-
private fun findViewByEmoji(root: View, emoji: String) =
mutableListOf<View>().apply {
findViewsById(
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt
new file mode 100644
index 0000000..5a102ac
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+
+/**
+ * Provides recently shared emoji. This is the default recent emoji list provider.
+ * Clients could specify the provider by their own.
+ */
+internal class DefaultRecentEmojiProvider(
+ context: Context
+) : RecentEmojiProvider {
+
+ companion object {
+ private const val PREF_KEY_RECENT_EMOJI = "pref_key_recent_emoji"
+ private const val RECENT_EMOJI_LIST_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+ private const val SPLIT_CHAR = ","
+ }
+
+ private val sharedPreferences =
+ context.getSharedPreferences(RECENT_EMOJI_LIST_FILE_NAME, MODE_PRIVATE)
+ private val recentEmojiList: MutableList<String> =
+ sharedPreferences.getString(PREF_KEY_RECENT_EMOJI, null)
+ ?.split(SPLIT_CHAR)
+ ?.toMutableList()
+ ?: mutableListOf()
+
+ override suspend fun getRecentItemList(): List<String> {
+ return recentEmojiList
+ }
+
+ override fun insert(emoji: String) {
+ recentEmojiList.remove(emoji)
+ recentEmojiList.add(0, emoji)
+ saveToPreferences()
+ }
+
+ private fun saveToPreferences() {
+ sharedPreferences
+ .edit()
+ .putString(PREF_KEY_RECENT_EMOJI, recentEmojiList.joinToString(SPLIT_CHAR))
+ .commit()
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 8356d6c..6820a15 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -25,10 +25,10 @@
import androidx.annotation.UiThread
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.util.Consumer
+import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.tracing.Trace
-import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
/** RecyclerView adapter for emoji body. */
internal class EmojiPickerBodyAdapter(
@@ -36,8 +36,12 @@
private val emojiGridColumns: Int,
private val emojiGridRows: Float,
private val categoryNames: Array<String>,
+ private val variantToBaseEmojiMap: Map<String, String>,
+ private val baseToVariantsEmojiMap: Map<String, List<String>>,
private val stickyVariantProvider: StickyVariantProvider,
- private val onEmojiPickedListener: Consumer<EmojiViewItem>?
+ private val onEmojiPickedListener: Consumer<EmojiViewItem>?,
+ private val recentEmojiList: MutableList<String>,
+ private val recentEmojiProvider: RecentEmojiProvider
) : Adapter<ViewHolder>() {
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
private var flattenSource: ItemViewDataFlatList
@@ -87,7 +91,24 @@
getParentWidth(parent) / emojiGridColumns,
(parent.measuredHeight / emojiGridRows).toInt(),
stickyVariantProvider,
- onEmojiPickedListener,
+ onEmojiPickedListener = { emojiViewItem ->
+ recentEmojiProvider.insert(emojiViewItem.emoji)
+ // update the recentEmojiList in the mean time
+ recentEmojiList.remove(emojiViewItem.emoji)
+ recentEmojiList.add(0, emojiViewItem.emoji)
+ onEmojiPickedListener?.accept(emojiViewItem)
+ // update the recent category to reload
+ this@EmojiPickerBodyAdapter.updateRecent(recentEmojiList.map { emoji ->
+ EmojiViewData(
+ RECENT_CATEGORY_INDEX,
+ recentEmojiList.indexOf(emoji),
+ emoji,
+ baseToVariantsEmojiMap[variantToBaseEmojiMap[emoji]]
+ ?.toTypedArray()
+ ?: arrayOf()
+ )
+ })
+ },
onEmojiPickedFromPopupListener = { emoji ->
(flattenSource[bindingAdapterPosition] as EmojiViewData).primary = emoji
notifyItemChanged(bindingAdapterPosition)
@@ -192,4 +213,12 @@
)
notifyDataSetChanged()
}
+
+ fun updateRecent(recents: List<ItemViewData>) {
+ flattenSource.updateSourcesByIndex(RECENT_CATEGORY_INDEX, recents)
+ notifyItemRangeChanged(
+ RECENT_CATEGORY_INDEX,
+ flattenSource.getCategorySize(RECENT_CATEGORY_INDEX)
+ )
+ }
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
deleted file mode 100644
index ffe1390..0000000
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
+++ /dev/null
@@ -1,51 +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.emoji2.emojipicker
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-
-/** Body view contains all emojis. */
-internal class EmojiPickerBodyView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null
-) : RecyclerView(context, attrs) {
-
- init {
- val layoutManager = GridLayoutManager(
- getContext(),
- EmojiPickerConstants.DEFAULT_BODY_COLUMNS,
- LinearLayoutManager.VERTICAL,
- /* reverseLayout = */ false
- )
- layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- val adapter = adapter ?: return 1
- val viewType = adapter.getItemViewType(position)
- // The following viewTypes occupy entire row.
- return if (
- viewType == CategorySeparatorViewData.TYPE ||
- viewType == EmptyCategoryViewData.TYPE
- ) EmojiPickerConstants.DEFAULT_BODY_COLUMNS else 1
- }
- }
- setLayoutManager(layoutManager)
- }
-}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 2abfc99..8f3f992 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -22,6 +22,7 @@
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.util.Consumer
+import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
@@ -62,6 +63,7 @@
}
private val stickyVariantProvider = StickyVariantProvider(context)
+ private var recentEmojiProvider = DefaultRecentEmojiProvider(context)
private lateinit var headerView: RecyclerView
private lateinit var bodyView: RecyclerView
@@ -93,13 +95,22 @@
emojiGridColumns: Int,
emojiGridRows: Float,
categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>,
- onEmojiPickedListener: Consumer<EmojiViewItem>?
+ variantToBaseEmojiMap: Map<String, String>,
+ baseToVariantsEmojiMap: Map<String, List<String>>,
+ onEmojiPickedListener: Consumer<EmojiViewItem>?,
+ recentEmojiList: MutableList<String>,
+ recentEmojiProvider: RecentEmojiProvider
): EmojiPickerBodyAdapter {
val categoryNames = mutableListOf<String>()
val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
// add recent category as the first row
categoryNames.add(resources.getString(R.string.emoji_category_recent))
- categorizedEmojis.add(mutableListOf())
+ categorizedEmojis.add(recentEmojiList.map { emoji ->
+ EmojiViewItem(
+ emoji,
+ baseToVariantsEmojiMap[variantToBaseEmojiMap[emoji]] ?: listOf()
+ )
+ }.toMutableList())
for (i in categorizedEmojiData.indices) {
categoryNames.add(categorizedEmojiData[i].categoryName)
@@ -116,8 +127,12 @@
emojiGridColumns,
emojiGridRows,
categoryNames.toTypedArray(),
+ variantToBaseEmojiMap,
+ baseToVariantsEmojiMap,
stickyVariantProvider,
- onEmojiPickedListener
+ onEmojiPickedListener,
+ recentEmojiList,
+ recentEmojiProvider
)
adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
@@ -165,14 +180,39 @@
// set bodyView
bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
+ bodyView.layoutManager = GridLayoutManager(
+ getContext(),
+ emojiGridColumns,
+ LinearLayoutManager.VERTICAL,
+ /* reverseLayout = */ false
+ ).apply {
+ spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+ override fun getSpanSize(position: Int): Int {
+ val adapter = bodyView.adapter ?: return 1
+ val viewType = adapter.getItemViewType(position)
+ // The following viewTypes occupy entire row.
+ return if (
+ viewType == CategorySeparatorViewData.TYPE ||
+ viewType == EmptyCategoryViewData.TYPE
+ ) emojiGridColumns else 1
+ }
+ }
+ }
val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
+ val variantToBaseEmojiMap = BundledEmojiListLoader.getPrimaryEmojiLookup()
+ val baseToVariantsEmojiMap = BundledEmojiListLoader.getEmojiVariantsLookup()
+ val recentEmojiList = recentEmojiProvider.getRecentItemList().toMutableList()
bodyView.adapter =
createEmojiPickerBodyAdapter(
context,
emojiGridColumns,
emojiGridRows,
categorizedEmojiData,
- onEmojiPickedListener
+ variantToBaseEmojiMap,
+ baseToVariantsEmojiMap,
+ onEmojiPickedListener,
+ recentEmojiList,
+ recentEmojiProvider
)
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
index a08fe8d..586f828 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -29,7 +29,6 @@
import android.widget.GridLayout
import android.widget.ImageView
import android.widget.PopupWindow
-import androidx.core.util.Consumer
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlin.math.roundToInt
@@ -40,15 +39,16 @@
width: Int,
height: Int,
stickyVariantProvider: StickyVariantProvider,
- onEmojiPickedListener: Consumer<EmojiViewItem>?,
+ onEmojiPickedListener: EmojiViewHolder.(EmojiViewItem) -> Unit,
onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
) : ViewHolder(
layoutInflater
.inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
) {
- private val onEmojiClickListener: OnClickListener = OnClickListener {
- // TODO(scduan): Add other on click events (e.g, add to recent)
- onEmojiPickedListener?.accept(emojiViewItem)
+ private val onEmojiClickListener: OnClickListener = OnClickListener { v ->
+ v.findViewById<EmojiView>(R.id.emoji_view).emoji?.let {
+ onEmojiPickedListener(EmojiViewItem(it.toString(), emojiViewItem.variants))
+ }
}
private val onEmojiLongClickListener: OnLongClickListener = OnLongClickListener {
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
index 45edc3c..8315a79 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
@@ -127,4 +127,29 @@
}
return currentCategoryIndex
}
+
+ fun updateSourcesByIndex(index: Int, sources: List<ItemViewData>) {
+ if (numberOfCategories == 0) {
+ Log.wtf(LOG_TAG, "Couldn't update due to empty categorizes sources")
+ return
+ }
+ categorizedSources[index] = sources
+ updateIndex()
+ }
+
+ @IntRange(from = 0)
+ fun getCategorySize(categoryIndex: Int): Int {
+ if (categoryIndex >= numberOfCategories) {
+ Log.wtf(
+ LOG_TAG,
+ String.format(
+ "Too large categoryIndex (%s vs %s)",
+ categoryIndex,
+ numberOfCategories
+ )
+ )
+ return 0
+ }
+ return categorySizes[categoryIndex]
+ }
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
new file mode 100644
index 0000000..56a756c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.emoji2.emojipicker
+
+/** An interface to provide recent emoji list. */
+internal interface RecentEmojiProvider {
+ /**
+ * Inserts an emoji into recent emoji list. Called by emoji picker when an emoji is shared.
+ */
+ fun insert(emoji: String)
+
+ /** Returns a list of recent items. */
+ suspend fun getRecentItemList(): List<String>
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
index f1bb622..84192cc 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -26,7 +26,7 @@
android:paddingEnd="@dimen/emoji_picker_header_padding"
android:paddingStart="@dimen/emoji_picker_header_padding" />
- <androidx.emoji2.emojipicker.EmojiPickerBodyView
+ <androidx.recyclerview.widget.RecyclerView
android:id="@+id/emoji_picker_body"
android:layout_width="match_parent"
android:layout_height="match_parent" />
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
index 21f04a3..8dc27b4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOSIEKONE EN EMOSIES"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"MENSE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DIERE EN NATUUR"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
index b3cbd77..4a4c6ac 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ሳቂታዎች እና ስሜቶች"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ሰዎች"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"እንስሳት እና ተፈጥሮ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ምልክቶች"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ባንዲራዎች"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ምንም ስሜት ገላጭ ምስሎች አይገኙም"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ምንም ስሜት ገላጭ ምስሎችን እስካሁን አልተጠቀሙም"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index 170353c..4a49b9d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"الوجوه المبتسمة والرموز التعبيرية"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"الأشخاص"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"الحيوانات والطبيعة"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"الرموز"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"الأعلام"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
index f1f6fbef..43191b0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"স্মাইলী আৰু আৱেগ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"মানুহ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"পশু আৰু প্ৰকৃতি"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"চিহ্ন"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"পতাকা"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনো ইম’জি উপলব্ধ নহয়"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপুনি এতিয়ালৈকে কোনো ইম’জি ব্যৱহাৰ কৰা নাই"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
index 78ed86d..1cc0ebb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAYLİK VƏ EMOSİYALAR"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ADAMLAR"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"HEYVANLAR VƏ TƏBİƏT"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SİMVOLLAR"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAQLAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Əlçatan emoji yoxdur"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hələ heç bir emojidən istifadə etməməsiniz"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
index ec5ddd5..f6d0248 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIJI I EMOCIJE"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"LJUDI"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
index eda0664..3777b3e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛІКІ І ЭМОЦЫІ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДЗІ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖЫВЁЛЫ І ПРЫРОДА"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"СІМВАЛЫ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"СЦЯГІ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма даступных эмодзі"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы пакуль не выкарыстоўвалі эмодзі"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
index d22d25b..5ba1de6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ЕМОТИКОНИ И ЕМОЦИИ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ХОРА"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТНИ И ПРИРОДА"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛИ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМЕНА"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма налични емоджи"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Все още не сте използвали емоджита"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
index 3fd2fae..3a9d0e2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"স্মাইলি ও আবেগ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ব্যক্তি"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"প্রাণী ও প্রকৃতি"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"প্রতীক"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ফ্ল্যাগ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনও ইমোজি উপলভ্য নেই"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপনি এখনও কোনও ইমোজি ব্যবহার করেননি"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
index d189ac9..64e766f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIJI I EMOCIJE"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"LJUDI"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
@@ -27,5 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji sličice nisu dostupne"</string>
- <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste upotrijebili emojije"</string>
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili nijednu emoji sličicu"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 4644a8b..2a08c9c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONES I EMOCIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONES"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS I NATURALESA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLS"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERES"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hi ha cap emoji disponible"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Encara no has fet servir cap emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
index f3932fe..534404e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLÍCI A EMOCE"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"LIDÉ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZVÍŘATA A PŘÍRODA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLY"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nejsou k dispozici žádné smajlíky"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatím jste žádná emodži nepoužili"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
index ca542b0..73744d7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS OG HUMØRIKONER"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DYR OG NATUR"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
index e836340..a3fdb2a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS UND EMOTIONEN"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONEN"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"TIERE UND NATUR"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLE"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGEN"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Keine Emojis verfügbar"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du hast noch keine Emojis verwendet"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
index 64e701f..1c1d893 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ΕΙΚΟΝΙΔΙΑ SMILEY ΚΑΙ ΣΥΝΑΙΣΘΗΜΑΤΑ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ΑΤΟΜΑ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ΖΩΑ ΚΑΙ ΦΥΣΗ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
index 23ba7f6..ad9f1c2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
@@ -17,6 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="emoji_category_recent" msgid="7142376595414250279">"RECENTLY USED"</string>
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
index 7cacbb0..976e527 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
@@ -17,6 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="emoji_category_recent" msgid="7142376595414250279">"RECENTLY USED"</string>
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
index 790b4e6..8432ac6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONES Y EMOCIONES"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALES Y NATURALEZA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLOS"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay ningún emoji disponible"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Todavía no usaste ningún emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
index 3f46a8d..9dbade8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONOS Y EMOCIONES"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALES Y NATURALEZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
index 2ea6dcd..102f061 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"NÄOIKOONID JA EMOTSIOONID"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"INIMESED"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"LOOMAD JA LOODUS"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SÜMBOLID"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"LIPUD"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ühtegi emotikoni pole saadaval"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Te pole veel ühtegi emotikoni kasutanud"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
index 71f27ef..8299aa5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"AURPEGIERAK ETA ALDARTEAK"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"JENDEA"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALIAK ETA NATURA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
index 8be716a..94ca207 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"شکلکها و احساسات"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"افراد"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"حیوانات و طبیعت"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"نمادها"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"پرچمها"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"اموجی دردسترس نیست"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"هنوز از هیچ اموجیای استفاده نکردهاید"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
index 31f6de1..e1388e0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"HYMIÖT JA TUNNETILAT"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"IHMISET"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ELÄIMET JA LUONTO"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLIT"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"LIPUT"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ei emojeita saatavilla"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Et ole vielä käyttänyt emojeita"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
index 2704128..02583ee 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ÉMOTICÔNES ET ÉMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONNES"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAUX ET NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
index 35a8d76..23f7e54 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ÉMOTICÔNES ET ÉMOTIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONNES"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAUX ET NATURE"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLES"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun emoji disponible"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez pas encore utilisé d\'emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
index 857e179..2a45e75 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ICONAS XESTUAIS E EMOTICONAS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSOAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLOS"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Non hai ningún emoji dispoñible"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aínda non utilizaches ningún emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
index 565cd02..1079b02 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"સ્માઇલી અને મનોભાવો"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"લોકો"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"પ્રાણીઓ અને પ્રકૃતિ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"પ્રતીકો"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ઝંડા"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"કોઈ ઇમોજી ઉપલબ્ધ નથી"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"તમે હજી સુધી કોઈ ઇમોજીનો ઉપયોગ કર્યો નથી"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
index c61cc03..d7f63c2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"स्माइली और भावनाएं"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"लोग"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"जानवर और प्रकृति"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"सिंबल"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"झंडे"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोई इमोजी उपलब्ध नहीं है"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"आपने अब तक किसी भी इमोजी का इस्तेमाल नहीं किया है"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
index 54b7e79..2e2076f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIĆI I EMOCIJE"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"OSOBE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
index 0848ad2..f0888ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"HANGULATJELEK ÉS HANGULATOK"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"SZEMÉLYEK"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ÁLLATOK ÉS TERMÉSZET"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SZIMBÓLUMOK"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ZÁSZLÓK"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nincsenek rendelkezésre álló emojik"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Még nem használt emojikat"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
index 4ac80b5..691fb63 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ԶՄԱՅԼԻԿՆԵՐ ԵՎ ՀՈՒԶԱՊԱՏԿԵՐԱԿՆԵՐ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ՄԱՐԴԻԿ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ԿԵՆԴԱՆԻՆԵՐ ԵՎ ԲՆՈՒԹՅՈՒՆ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ՆՇԱՆՆԵՐ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ԴՐՈՇՆԵՐ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Հասանելի էմոջիներ չկան"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Դուք դեռ չեք օգտագործել էմոջիներ"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
index 7b4f962..d014a1e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY DAN EMOTIKON"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ORANG"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"HEWAN DAN ALAM"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOL"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tidak ada emoji yang tersedia"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan emoji apa pun"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
index f743f26..40c34d8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"BROSKARLAR OG TILFINNINGAR"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"FÓLK"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DÝR OG NÁTTÚRA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"TÁKN"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FÁNAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Engin emoji-tákn í boði"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Þú hefur ekki notað nein emoji enn"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
index 9f9b707..8dd0e79 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILE ED EMOZIONI"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALI E NATURA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BANDIERE"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nessuna emoji disponibile"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Non hai ancora usato alcuna emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
index db00c09..31051ea 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"סמיילי ואמוטיקונים"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"אנשים"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"בעלי חיים וטבע"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"סמלים"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"דגלים"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"אין סמלי אמוג\'י זמינים"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"עדיין לא השתמשת באף אמוג\'י"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
index cbccd1f..570140e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"顔文字、気分"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物、自然"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
index 3e33c41..b069aae 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"სიცილაკები და ემოციები"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ადამიანები"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ცხოველები და ბუნება"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
index 22c8157..e6784a8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛДАР МЕН ЭМОЦИЯЛАР"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"АДАМДАР"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖАНУАРЛАР ЖӘНЕ ТАБИҒАТ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ТАҢБАЛАР"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ЖАЛАУШАЛАР"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Эмоджи жоқ"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Әлі ешқандай эмоджи пайдаланылған жоқ."</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
index b79617d..11c09d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"រូបទឹកមុខ និងអារម្មណ៍"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"មនុស្ស"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"សត្វ និងធម្មជាតិ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
index 51131c5..e1e11ed 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ಸ್ಮೈಲಿಗಳು ಮತ್ತು ಭಾವನೆಗಳು"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ಜನರು"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ಪ್ರಾಣಿಗಳು ಮತ್ತು ಪ್ರಕೃತಿ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ಸಂಕೇತಗಳು"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ಫ್ಲ್ಯಾಗ್ಗಳು"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ಯಾವುದೇ ಎಮೊಜಿಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಎಮೋಜಿಗಳನ್ನು ಬಳಸಿಲ್ಲ"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
index d482493..0cf33ae 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"이모티콘 및 감정"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"사람"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"동물 및 자연"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"기호"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"깃발"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"사용 가능한 그림 이모티콘 없음"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"아직 사용한 이모티콘이 없습니다."</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
index cf5d505..185dd34 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"БЫЙТЫКЧАЛАР ЖАНА ЭМОЦИЯЛАР"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"АДАМДАР"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖАНЫБАРЛАР ЖАНА ЖАРАТЫЛЫШ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛДОР"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ЖЕЛЕКТЕР"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Жеткиликтүү быйтыкчалар жок"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Азырынча быйтыкчаларды колдоно элексиз"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
index 32ac1ec..4472672 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ໜ້າຍິ້ມ ແລະ ຄວາມຮູ້ສຶກ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ຜູ້ຄົນ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ສັດ ແລະ ທຳມະຊາດ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ສັນຍາລັກ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ທຸງ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ບໍ່ມີອີໂມຈິໃຫ້ນຳໃຊ້"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ທ່ານຍັງບໍ່ໄດ້ໃຊ້ອີໂມຈິໃດເທື່ອ"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
index 8c90dcb..1f11862 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"JAUSTUKAI IR EMOCIJOS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ŽMONĖS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"GYVŪNAI IR GAMTA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLIAI"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"VĖLIAVOS"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nėra jokių pasiekiamų jaustukų"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Dar nenaudojote jokių jaustukų"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
index c6e30c7..f5f63fb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAIDIŅI UN EMOCIJAS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DZĪVNIEKI UN DABA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"KAROGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nav pieejamu emocijzīmju"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jūs vēl neesat izmantojis nevienu emocijzīmi"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
index 85ac3d7..79693ce 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМЕШКОВЦИ И ЕМОТИКОНИ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ЛУЃЕ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТНИ И ПРИРОДА"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
index dd073d9..9dc3db3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"സ്മൈലികളും ഇമോഷനുകളും"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ആളുകൾ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"മൃഗങ്ങളും പ്രകൃതിയും"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ചിഹ്നങ്ങൾ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"പതാകകൾ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ഇമോജികളൊന്നും ലഭ്യമല്ല"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"നിങ്ങൾ ഇതുവരെ ഇമോജികളൊന്നും ഉപയോഗിച്ചിട്ടില്ല"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
index 53792c9..de3c4dc 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ИНЭЭМСЭГЛЭЛ БОЛОН СЭТГЭЛ ХӨДЛӨЛ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ХҮМҮҮС"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"АМЬТАД БА БАЙГАЛЬ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
index 82109e0..4af8d70 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"स्मायली आणि भावना"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"लोक"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"प्राणी आणि निसर्ग"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"चिन्हे"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ध्वज"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोणतेही इमोजी उपलब्ध नाहीत"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तुम्ही अद्याप कोणतेही इमोजी वापरलेले नाहीत"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
index 1462102..d59b51e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY DAN EMOSI"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ORANG"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAIWAN DAN ALAM SEMULA JADI"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
index 536c277..bd58d4c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"စမိုင်းလီနှင့် ခံစားချက်များ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"လူများ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"တိရစ္ဆာန်များနှင့် သဘာဝ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"သင်္ကေတများ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"အလံများ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"အီမိုဂျီ မရနိုင်ပါ"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"အီမိုဂျီ အသုံးမပြုသေးပါ"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
index f050c34..04a123b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEFJES OG UTTRYKKSIKONER"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DYR OG NATUR"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLER"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGG"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ingen emojier er tilgjengelige"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brukt noen emojier ennå"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
index 97739a0..163107c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"स्माइली र भावनाहरू"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"मान्छेहरू"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"पशु र प्रकृति"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"चिन्हहरू"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"झन्डाहरू"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कुनै पनि इमोजी उपलब्ध छैन"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तपाईंले हालसम्म कुनै पनि इमोजी प्रयोग गर्नुभएको छैन"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
index 6c9d4c3..d00dbdd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS EN EMOTIES"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"MENSEN"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DIEREN EN NATUUR"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLEN"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAGGEN"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emoji\'s beschikbaar"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Je hebt nog geen emoji\'s gebruikt"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
index ae32bb3..0fea003 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ସ୍ମାଇଲି ଓ ଆବେଗଗୁଡ଼ିକ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ଲୋକମାନେ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ଜୀବଜନ୍ତୁ ଓ ପ୍ରକୃତି"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
index 72e0b6a..40520be 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ਸਮਾਈਲੀ ਅਤੇ ਜਜ਼ਬਾਤ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ਲੋਕ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ਜਾਨਵਰ ਅਤੇ ਕੁਦਰਤ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ਚਿੰਨ੍ਹ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ਝੰਡੇ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ਕੋਈ ਇਮੋਜੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕਿਸੇ ਵੀ ਇਮੋਜੀ ਦੀ ਵਰਤੋਂ ਨਹੀਂ ਕੀਤੀ ਹੈ"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
index e4d8310..e07c706 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTIKONY"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"UCZESTNICY"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZWIERZĘTA I PRZYRODA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLE"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Brak dostępnych emotikonów"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Żadne emotikony nie zostały jeszcze użyte"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
index 07507fb..4a7a8d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"CARINHAS E EMOTICONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
index e6e29b1..dfe0044 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONS E ÍCONES EXPRESSIVOS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
index 07507fb..4a7a8d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"CARINHAS E EMOTICONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
index 21686b6..b704db4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICOANE ȘI EMOȚII"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSOANE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALE ȘI NATURĂ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
index b7a9857..7e5756f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛИКИ И ЭМОЦИИ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДИ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ПРИРОДА И ЖИВОТНЫЕ"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛЫ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ФЛАГИ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нет доступных эмодзи"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы ещё не использовали эмодзи"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
index e1bfcea..f0a6246 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"සිනාසීම් සහ චිත්තවේග"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"පුද්ගලයින්"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"සතුන් හා ස්වභාවධර්මය"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"සංකේත"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ධජ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ඉමොජි කිසිවක් නොලැබේ"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ඔබ තවමත් කිසිදු ඉමෝජියක් භාවිතා කර නැත"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
index 3a557ab..ae90524 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLÍKY A EMOTIKONY"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ĽUDIA"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZVIERATÁ A PRÍRODA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLY"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nie sú k dispozícii žiadne emodži"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatiaľ ste nepoužili žiadne emodži"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
index 91261f4..3fab8dd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ČUSTVENI SIMBOLI IN ČUSTVA"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"OSEBE"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVALI IN NARAVA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
index 532209d..9abcb71 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"BUZËQESHJE DHE EMOCIONE"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"NJERËZ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"KAFSHË DHE NATYRË"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLE"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAMUJ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nuk ofrohen emoji"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Nuk ke përdorur ende asnjë emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
index f899101..94321de 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЈЛИЈИ И ЕМОЦИЈЕ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ЉУДИ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТИЊЕ И ПРИРОДА"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
index 9ba51c6..9c8815d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"KÄNSLOIKONER OCH KÄNSLOR"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"DJUR OCH NATUR"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLER"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGOR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Inga emojier tillgängliga"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ännu inte använt emojis"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
index a2b3651..0d6aeb9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"VICHESHI NA HISIA"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"WATU"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"WANYAMA NA MAZINGIRA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"ISHARA"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hakuna emoji zinazopatikana"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bado hujatumia emoji zozote"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
index 6a24080..1ae568f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"ஸ்மைலிகளும் எமோடிகான்களும்"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"நபர்"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"விலங்குகளும் இயற்கையும்"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"சின்னங்கள்"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"கொடிகள்"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ஈமோஜிகள் எதுவுமில்லை"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"இதுவரை ஈமோஜி எதையும் நீங்கள் பயன்படுத்தவில்லை"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
index 228b252..115f983 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"స్మైలీలు, ఎమోషన్లు"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"వ్యక్తులు"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"జంతువులు, ప్రకృతి"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"గుర్తులు"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ఫ్లాగ్లు"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ఎమోజీలు ఏవీ అందుబాటులో లేవు"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"మీరు ఇంకా ఎమోజీలు ఏవీ ఉపయోగించలేదు"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
index 4718adb..839d759 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"หน้ายิ้มและอารมณ์"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ผู้คน"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"สัตว์และธรรมชาติ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
index d28f9b7..794701d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"MGA SMILEY AT MGA EMOSYON"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"MGA TAO"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"MGA HAYOP AT KALIKASAN"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
index f3dd488..a27d6b2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY\'LER VE İFADELER"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"İNSANLAR"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAYVANLAR VE DOĞA"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"SEMBOLLER"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAKLAR"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Kullanılabilir emoji yok"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Henüz emoji kullanmadınız"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
index a1d8ff6..ff9adf2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛИКИ Й ЕМОЦІЇ"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДИ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ТВАРИНИ ТА ПРИРОДА"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛИ"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"ПРАПОРИ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Немає смайлів"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ви ще не використовували смайли"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
index 212835f..110b3cb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"اسمائلیز اور جذبات"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"لوگ"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"جانور اور قدرت"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"علامات"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"جھنڈے"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"کوئی بھی ایموجی دستیاب نہیں ہے"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"آپ نے ابھی تک کوئی بھی ایموجی استعمال نہیں کی ہے"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
index 02613bd..0b0feff 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"KULGICH VA EMOJILAR"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ODAMLAR"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAYVONLAR VA TABIAT"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
index baba3bc..41fef1b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"MẶT CƯỜI VÀ BIỂU TƯỢNG CẢM XÚC"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"MỌI NGƯỜI"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ĐỘNG VẬT VÀ THIÊN NHIÊN"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"BIỂU TƯỢNG"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"CỜ"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Không có biểu tượng cảm xúc nào"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bạn chưa sử dụng biểu tượng cảm xúc nào"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
index 385b281..2761523 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"表情符号"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"动物和自然"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"符号"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"旗帜"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"没有可用的表情符号"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用过任何表情符号"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
index 68206d5..b1eabfb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"表情符號"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物和大自然"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"符號"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的 Emoji"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用任何 Emoji"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
index 7892cef..fb39886 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"表情符號"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物與大自然"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"符號"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的表情符號"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何表情符號"</string>
</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
index 6c422c9..2fbc5e6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
@@ -17,6 +17,8 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+ <skip />
<string name="emoji_category_emotions" msgid="1570830970240985537">"AMASMAYILI NEMIZWA"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"ABANTU"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"IZILWANE NENDALO"</string>
@@ -27,6 +29,5 @@
<string name="emoji_category_symbols" msgid="5626171724310261787">"AMASIMBULI"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"AMAFULEGI"</string>
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Awekho ama-emoji atholakalayo"</string>
- <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
- <skip />
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Awukasebenzisi noma yimaphi ama-emoji okwamanje"</string>
</resources>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 11e4050..d7c6e4d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -194,10 +194,10 @@
okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
playCore = { module = "com.google.android.play:core", version = "1.10.3" }
-playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.3.0"}
+playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.4.0"}
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" }
-playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0-beta"}
+playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0"}
playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
index f655aec..f5dc978 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
@@ -25,7 +25,7 @@
import androidx.annotation.RestrictTo;
/**
- * Simple interface for predicting ink points.
+ * Simple interface for predicting motion points.
*
* @hide
*/
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index f5929e0..415e2da 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -117,7 +117,8 @@
final int pointerCount = mPredictorMap.size();
// Shortcut for likely case where only zero or one pointer is on the screen
// this logic exists only to make sure logic when one pointer is on screen then
- // there is no performance degradation of using MultiPointerPredictor vs KalmanInkPredictor
+ // there is no performance degradation of using MultiPointerPredictor vs
+ // SinglePointerPredictor
// TODO: verify performance is not degraded by removing this shortcut logic.
if (pointerCount == 0) {
if (DEBUG_PREDICTION) {
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
index 0656734..db4ba103 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
@@ -35,7 +35,7 @@
*/
@RestrictTo(LIBRARY)
public class SinglePointerPredictor implements KalmanPredictor {
- private static final String TAG = "KalmanInkPredictor";
+ private static final String TAG = "SinglePointerPredictor";
// Influence of jank during each prediction sample
private static final float JANK_INFLUENCE = 0.1f;
@@ -86,7 +86,7 @@
private double mPressure = 0;
/**
- * Kalman based ink predictor, predicting the location of the pen `predictionTarget`
+ * Kalman based predictor, predicting the location of the pen `predictionTarget`
* milliseconds into the future.
*
* <p>This filter can provide solid prediction up to 25ms into the future. If you are not
diff --git a/libraryversions.toml b/libraryversions.toml
index 23d4145..9215b18 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -9,7 +9,7 @@
ARCH_CORE = "2.2.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
AUTOFILL = "1.2.0-beta02"
-BENCHMARK = "1.2.0-alpha07"
+BENCHMARK = "1.2.0-alpha08"
BIOMETRIC = "1.2.0-alpha06"
BLUETOOTH = "1.0.0-alpha01"
BROWSER = "1.5.0-alpha02"
@@ -45,7 +45,7 @@
CURSORADAPTER = "1.1.0-alpha01"
CUSTOMVIEW = "1.2.0-alpha03"
CUSTOMVIEW_POOLINGCONTAINER = "1.1.0-alpha01"
-DATASTORE = "1.1.0-alpha01"
+DATASTORE = "1.1.0-alpha02"
DATASTORE_KMP = "1.1.0-dev01"
DOCUMENTFILE = "1.1.0-alpha02"
DRAGANDDROP = "1.1.0-alpha01"
@@ -85,7 +85,7 @@
MEDIA2 = "1.3.0-alpha01"
MEDIAROUTER = "1.4.0-alpha02"
METRICS = "1.0.0-alpha04"
-NAVIGATION = "2.6.0-alpha04"
+NAVIGATION = "2.6.0-alpha05"
PAGING = "3.2.0-alpha04"
PAGING_COMPOSE = "1.0.0-alpha18"
PALETTE = "1.1.0-alpha01"
@@ -95,7 +95,7 @@
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
PRIVACYSANDBOX_TOOLS = "1.0.0-alpha02"
PRIVACYSANDBOX_UI = "1.0.0-alpha01"
-PROFILEINSTALLER = "1.3.0-alpha02"
+PROFILEINSTALLER = "1.3.0-alpha03"
RECOMMENDATION = "1.1.0-alpha01"
RECYCLERVIEW = "1.4.0-alpha01"
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
@@ -103,7 +103,7 @@
RESOURCEINSPECTION = "1.1.0-alpha01"
ROOM = "2.6.0-alpha01"
SAVEDSTATE = "1.3.0-alpha01"
-SECURITY = "1.1.0-alpha04"
+SECURITY = "1.1.0-alpha05"
SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
SECURITY_BIOMETRIC = "1.0.0-alpha01"
@@ -123,7 +123,7 @@
TEST_UIAUTOMATOR = "2.3.0-alpha02"
TEXT = "1.0.0-alpha01"
TRACING = "1.2.0-alpha02"
-TRACING_PERFETTO = "1.0.0-alpha07"
+TRACING_PERFETTO = "1.0.0-alpha08"
TRANSITION = "1.5.0-alpha01"
TV = "1.0.0-alpha03"
TVPROVIDER = "1.1.0-alpha02"
@@ -140,6 +140,7 @@
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha01"
WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
+WEAR_PROTOLAYOUT = "1.0.0-alpha01"
WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
WEAR_TILES = "1.2.0-alpha01"
WEAR_WATCHFACE = "1.2.0-alpha05"
@@ -254,6 +255,7 @@
VIEWPAGER2 = { group = "androidx.viewpager2", atomicGroupVersion = "versions.VIEWPAGER2" }
WEAR = { group = "androidx.wear" }
WEAR_COMPOSE = { group = "androidx.wear.compose", atomicGroupVersion = "versions.WEAR_COMPOSE" }
+WEAR_PROTOLAYOUT = { group = "androidx.wear.protolayout", atomicGroupVersion = "versions.WEAR_PROTOLAYOUT" }
WEAR_TILES = { group = "androidx.wear.tiles", atomicGroupVersion = "versions.WEAR_TILES" }
WEAR_WATCHFACE = { group = "androidx.wear.watchface", atomicGroupVersion = "versions.WEAR_WATCHFACE" }
WEBKIT = { group = "androidx.webkit", atomicGroupVersion = "versions.WEBKIT" }
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 25dd589..c9e8b12 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -17,7 +17,7 @@
package androidx.privacysandbox.tools.apicompiler.generator
import androidx.privacysandbox.tools.core.generator.build
-import androidx.privacysandbox.tools.core.generator.poetSpec
+import androidx.privacysandbox.tools.core.generator.poetTypeName
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.getOnlyService
@@ -77,7 +77,7 @@
return FunSpec.builder(createServiceFunctionName(service))
.addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
.addParameter("context", contextClass)
- .returns(service.type.poetSpec())
+ .returns(service.type.poetTypeName())
.build()
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
index 454d7b8..71b9761 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -52,9 +52,19 @@
"com/mysdk/IResponseTransactionCallback.java",
"com/mysdk/IStringTransactionCallback.java",
"com/mysdk/IUnitTransactionCallback.java",
+ "com/mysdk/IListResponseTransactionCallback.java",
+ "com/mysdk/IListIntTransactionCallback.java",
+ "com/mysdk/IListLongTransactionCallback.java",
+ "com/mysdk/IListDoubleTransactionCallback.java",
+ "com/mysdk/IListStringTransactionCallback.java",
+ "com/mysdk/IListBooleanTransactionCallback.java",
+ "com/mysdk/IListFloatTransactionCallback.java",
+ "com/mysdk/IListCharTransactionCallback.java",
+ "com/mysdk/IListShortTransactionCallback.java",
"com/mysdk/ParcelableRequest.java",
"com/mysdk/ParcelableResponse.java",
"com/mysdk/ParcelableStackFrame.java",
+ "com/mysdk/ParcelableInnerValue.java",
"com/mysdk/PrivacySandboxThrowableParcel.java",
)
assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
index 94ec579..1ba71b6 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
@@ -35,11 +35,33 @@
@PrivacySandboxInterface
interface MySecondInterface {
- fun doMoreStuff(x: Int)
+ suspend fun doIntStuff(x: List<Int>): List<Int>
+
+ suspend fun doCharStuff(x: List<Char>): List<Char>
+
+ suspend fun doFloatStuff(x: List<Float>): List<Float>
+
+ suspend fun doLongStuff(x: List<Long>): List<Long>
+
+ suspend fun doDoubleStuff(x: List<Double>): List<Double>
+
+ suspend fun doBooleanStuff(x: List<Boolean>): List<Boolean>
+
+ suspend fun doShortStuff(x: List<Short>): List<Short>
+
+ suspend fun doStringStuff(x: List<String>): List<String>
+
+ suspend fun doValueStuff(x: List<Request>): List<Response>
}
@PrivacySandboxValue
-data class Request(val query: String, val myInterface: MyInterface)
+data class Request(
+ val query: String,
+ val extraValues: List<InnerValue>,
+ val myInterface: MyInterface)
+
+@PrivacySandboxValue
+data class InnerValue(val numbers: List<Int>)
@PrivacySandboxValue
data class Response(val response: String, val mySecondInterface: MySecondInterface)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt
new file mode 100644
index 0000000..3717090
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt
@@ -0,0 +1,15 @@
+package com.mysdk
+
+public object InnerValueConverter {
+ public fun fromParcelable(parcelable: ParcelableInnerValue): InnerValue {
+ val annotatedValue = InnerValue(
+ numbers = parcelable.numbers.toList())
+ return annotatedValue
+ }
+
+ public fun toParcelable(annotatedValue: InnerValue): ParcelableInnerValue {
+ val parcelable = ParcelableInnerValue()
+ parcelable.numbers = annotatedValue.numbers.toIntArray()
+ return parcelable
+ }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
index a871395..494c80c 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
@@ -1,12 +1,167 @@
package com.mysdk
-import kotlin.Int
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
+import com.mysdk.RequestConverter.fromParcelable
+import com.mysdk.ResponseConverter.toParcelable
+import kotlin.Array
+import kotlin.BooleanArray
+import kotlin.CharArray
+import kotlin.DoubleArray
+import kotlin.FloatArray
+import kotlin.IntArray
+import kotlin.LongArray
+import kotlin.String
import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
public class MySecondInterfaceStubDelegate internal constructor(
public val `delegate`: MySecondInterface,
) : IMySecondInterface.Stub() {
- public override fun doMoreStuff(x: Int): Unit {
- delegate.doMoreStuff(x)
+ public override fun doIntStuff(x: IntArray, transactionCallback: IListIntTransactionCallback):
+ Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doIntStuff(x.toList())
+ transactionCallback.onSuccess(result.toIntArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doCharStuff(x: CharArray, transactionCallback: IListCharTransactionCallback):
+ Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doCharStuff(x.toList())
+ transactionCallback.onSuccess(result.toCharArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doFloatStuff(x: FloatArray,
+ transactionCallback: IListFloatTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doFloatStuff(x.toList())
+ transactionCallback.onSuccess(result.toFloatArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doLongStuff(x: LongArray, transactionCallback: IListLongTransactionCallback):
+ Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doLongStuff(x.toList())
+ transactionCallback.onSuccess(result.toLongArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doDoubleStuff(x: DoubleArray,
+ transactionCallback: IListDoubleTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doDoubleStuff(x.toList())
+ transactionCallback.onSuccess(result.toDoubleArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doBooleanStuff(x: BooleanArray,
+ transactionCallback: IListBooleanTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doBooleanStuff(x.toList())
+ transactionCallback.onSuccess(result.toBooleanArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doShortStuff(x: IntArray, transactionCallback: IListShortTransactionCallback):
+ Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doShortStuff(x.map { it.toShort() }.toList())
+ transactionCallback.onSuccess(result.map { it.toInt() }.toIntArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doStringStuff(x: Array<String>,
+ transactionCallback: IListStringTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doStringStuff(x.toList())
+ transactionCallback.onSuccess(result.toTypedArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doValueStuff(x: Array<ParcelableRequest>,
+ transactionCallback: IListResponseTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doValueStuff(x.map { fromParcelable(it) }.toList())
+ transactionCallback.onSuccess(result.map { toParcelable(it) }.toTypedArray())
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(toThrowableParcel(t))
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
}
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
index 538f1cd..da8b7cf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
@@ -4,6 +4,8 @@
public fun fromParcelable(parcelable: ParcelableRequest): Request {
val annotatedValue = Request(
query = parcelable.query,
+ extraValues = parcelable.extraValues.map {
+ com.mysdk.InnerValueConverter.fromParcelable(it) }.toList(),
myInterface = (parcelable.myInterface as MyInterfaceStubDelegate).delegate)
return annotatedValue
}
@@ -11,6 +13,8 @@
public fun toParcelable(annotatedValue: Request): ParcelableRequest {
val parcelable = ParcelableRequest()
parcelable.query = annotatedValue.query
+ parcelable.extraValues = annotatedValue.extraValues.map {
+ com.mysdk.InnerValueConverter.toParcelable(it) }.toTypedArray()
parcelable.myInterface = MyInterfaceStubDelegate(annotatedValue.myInterface)
return parcelable
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
index 5394501..02ecc7d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
@@ -20,17 +20,19 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.poetClassName
import androidx.privacysandbox.tools.core.generator.poetSpec
+import androidx.privacysandbox.tools.core.generator.poetTypeName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.TypeSpec
-internal class InterfaceFileGenerator() {
+internal class InterfaceFileGenerator {
fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
val annotatedInterfaceType =
- TypeSpec.interfaceBuilder(annotatedInterface.type.poetSpec()).build {
+ TypeSpec.interfaceBuilder(annotatedInterface.type.poetClassName()).build {
addFunctions(annotatedInterface.methods.map(::generateInterfaceMethod))
}
@@ -47,6 +49,6 @@
addModifiers(KModifier.SUSPEND)
}
addParameters(method.parameters.map { it.poetSpec() })
- returns(method.returnType.poetSpec())
+ returns(method.returnType.poetTypeName())
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
index 955a26b..7437b18 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
@@ -29,6 +29,14 @@
"com/mysdk/IBooleanTransactionCallback.java",
"com/mysdk/ICancellationSignal.java",
"com/mysdk/IUnitTransactionCallback.java",
+ "com/mysdk/IListLongTransactionCallback.java",
+ "com/mysdk/IListDoubleTransactionCallback.java",
+ "com/mysdk/IListShortTransactionCallback.java",
+ "com/mysdk/IListStringTransactionCallback.java",
+ "com/mysdk/IListBooleanTransactionCallback.java",
+ "com/mysdk/IListFloatTransactionCallback.java",
+ "com/mysdk/IListCharTransactionCallback.java",
+ "com/mysdk/IListIntTransactionCallback.java",
"com/mysdk/ParcelableStackFrame.java",
"com/mysdk/PrivacySandboxThrowableParcel.java",
)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
index 6eb5997..c729c44 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
@@ -28,6 +28,7 @@
"com/sdkwithvalues/IMyInterface.java",
"com/sdkwithvalues/ISdkInterface.java",
"com/sdkwithvalues/ISdkResponseTransactionCallback.java",
+ "com/sdkwithvalues/IListSdkResponseTransactionCallback.java",
"com/sdkwithvalues/ParcelableInnerSdkValue.java",
"com/sdkwithvalues/ParcelableSdkRequest.java",
"com/sdkwithvalues/ParcelableSdkResponse.java",
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
index 666c0c3..4ba4d45 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
@@ -25,4 +25,20 @@
suspend fun doSomethingAsync(first: Int, second: String, third: Long): Boolean
suspend fun receiveAndReturnNothingAsync()
+
+ suspend fun processIntList(x: List<Int>): List<Int>
+
+ suspend fun processCharList(x: List<Char>): List<Char>
+
+ suspend fun processFloatList(x: List<Float>): List<Float>
+
+ suspend fun processLongList(x: List<Long>): List<Long>
+
+ suspend fun processDoubleList(x: List<Double>): List<Double>
+
+ suspend fun processBooleanList(x: List<Boolean>): List<Boolean>
+
+ suspend fun processShortList(x: List<Short>): List<Short>
+
+ suspend fun processStringList(x: List<String>): List<String>
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
index c6132b1..e63fc50 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
@@ -21,6 +21,22 @@
public fun echoString(input: String): Unit
+ public suspend fun processBooleanList(x: List<Boolean>): List<Boolean>
+
+ public suspend fun processCharList(x: List<Char>): List<Char>
+
+ public suspend fun processDoubleList(x: List<Double>): List<Double>
+
+ public suspend fun processFloatList(x: List<Float>): List<Float>
+
+ public suspend fun processIntList(x: List<Int>): List<Int>
+
+ public suspend fun processLongList(x: List<Long>): List<Long>
+
+ public suspend fun processShortList(x: List<Short>): List<Short>
+
+ public suspend fun processStringList(x: List<String>): List<String>
+
public fun receiveAndReturnNothing(): Unit
public suspend fun receiveAndReturnNothingAsync(): Unit
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
index 2d5b21d..6a52938 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
@@ -62,6 +62,190 @@
remote.echoString(input)
}
+ public override suspend fun processBooleanList(x: List<Boolean>): List<Boolean> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListBooleanTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: BooleanArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processBooleanList(x.toBooleanArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processCharList(x: List<Char>): List<Char> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListCharTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: CharArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processCharList(x.toCharArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processDoubleList(x: List<Double>): List<Double> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListDoubleTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: DoubleArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processDoubleList(x.toDoubleArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processFloatList(x: List<Float>): List<Float> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListFloatTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: FloatArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processFloatList(x.toFloatArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processIntList(x: List<Int>): List<Int> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListIntTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: IntArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processIntList(x.toIntArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processLongList(x: List<Long>): List<Long> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListLongTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: LongArray) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processLongList(x.toLongArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processShortList(x: List<Short>): List<Short> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListShortTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: IntArray) {
+ it.resumeWith(Result.success(result.map { it.toShort() }.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processShortList(x.map { it.toInt() }.toIntArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override suspend fun processStringList(x: List<String>): List<String> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListStringTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: Array<String>) {
+ it.resumeWith(Result.success(result.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processStringList(x.toTypedArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
public override fun receiveAndReturnNothing(): Unit {
remote.receiveAndReturnNothing()
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
index 6265564..4781b61 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
@@ -7,6 +7,8 @@
@PrivacySandboxService
interface SdkInterface {
suspend fun exampleMethod(request: SdkRequest): SdkResponse
+
+ suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse>
}
@PrivacySandboxValue
@@ -19,10 +21,15 @@
val floatingPoint: Float,
val hugeNumber: Double,
val myInterface: MyInterface,
+ val numbers: List<Int>,
)
@PrivacySandboxValue
-data class SdkRequest(val id: Long, val innerValue: InnerSdkValue)
+data class SdkRequest(
+ val id: Long,
+ val innerValue: InnerSdkValue,
+ val moreValues: List<InnerSdkValue>
+)
@PrivacySandboxValue
data class SdkResponse(val success: Boolean, val originalRequest: SdkRequest)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
index dc6d855..4b99a0b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
@@ -7,6 +7,7 @@
public val id: Int,
public val message: String,
public val myInterface: MyInterface,
+ public val numbers: List<Int>,
public val separator: Char,
public val shouldBeAwesome: Boolean,
)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
index ffa0b12..71ffd8a 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
@@ -9,6 +9,7 @@
id = parcelable.id,
message = parcelable.message,
myInterface = MyInterfaceClientProxy(parcelable.myInterface),
+ numbers = parcelable.numbers.toList(),
separator = parcelable.separator,
shouldBeAwesome = parcelable.shouldBeAwesome)
return annotatedValue
@@ -22,6 +23,7 @@
parcelable.id = annotatedValue.id
parcelable.message = annotatedValue.message
parcelable.myInterface = (annotatedValue.myInterface as MyInterfaceClientProxy).remote
+ parcelable.numbers = annotatedValue.numbers.toIntArray()
parcelable.separator = annotatedValue.separator
parcelable.shouldBeAwesome = annotatedValue.shouldBeAwesome
return parcelable
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
index 2912b22..89d0d9b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
@@ -2,4 +2,6 @@
public interface SdkInterface {
public suspend fun exampleMethod(request: SdkRequest): SdkResponse
+
+ public suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse>
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
index 1d6fada..ae8358f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
@@ -1,7 +1,10 @@
package com.sdkwithvalues
+import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter
import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import com.sdkwithvalues.SdkRequestConverter
import com.sdkwithvalues.SdkRequestConverter.toParcelable
+import com.sdkwithvalues.SdkResponseConverter
import com.sdkwithvalues.SdkResponseConverter.fromParcelable
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -31,4 +34,27 @@
mCancellationSignal?.cancel()
}
}
+
+ public override suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse> =
+ suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IListSdkResponseTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: Array<ParcelableSdkResponse>) {
+ it.resumeWith(Result.success(result.map { fromParcelable(it) }.toList()))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.processValueList(x.map { toParcelable(it) }.toTypedArray(), transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
index 8909acc..3d82a7b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
@@ -3,4 +3,5 @@
public data class SdkRequest(
public val id: Long,
public val innerValue: InnerSdkValue,
+ public val moreValues: List<InnerSdkValue>,
)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
index 4ada52a..a652ad2 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
@@ -5,7 +5,9 @@
val annotatedValue = SdkRequest(
id = parcelable.id,
innerValue =
- com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(parcelable.innerValue))
+ com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(parcelable.innerValue),
+ moreValues = parcelable.moreValues.map {
+ com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(it) }.toList())
return annotatedValue
}
@@ -14,6 +16,8 @@
parcelable.id = annotatedValue.id
parcelable.innerValue =
com.sdkwithvalues.InnerSdkValueConverter.toParcelable(annotatedValue.innerValue)
+ parcelable.moreValues = annotatedValue.moreValues.map {
+ com.sdkwithvalues.InnerSdkValueConverter.toParcelable(it) }.toTypedArray()
return parcelable
}
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
index 1df8157..650c3ca 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
@@ -123,10 +123,11 @@
check(parameter.type != Types.unit) {
"Void cannot be a parameter type."
}
+ val aidlType = getAidlTypeDeclaration(parameter.type)
addParameter(
parameter.name,
- getAidlTypeDeclaration(parameter.type),
- isIn = api.valueMap.containsKey(parameter.type)
+ aidlType,
+ isIn = api.valueMap.containsKey(parameter.type) || aidlType.isList
)
}
@@ -229,6 +230,7 @@
// TODO: AIDL doesn't support short, make sure it's handled correctly.
Short::class.qualifiedName -> primitive("int")
Unit::class.qualifiedName -> primitive("void")
+ List::class.qualifiedName -> getAidlTypeDeclaration(type.typeParameters[0]).listSpec()
else -> throw IllegalArgumentException(
"Unsupported type conversion ${type.qualifiedName}"
)
@@ -249,7 +251,8 @@
fun AnnotatedInterface.aidlName() = "I${type.simpleName}"
-fun Type.transactionCallbackName() = "I${simpleName}TransactionCallback"
+fun Type.transactionCallbackName() =
+ "I${simpleName}${typeParameters.joinToString("") { it.simpleName }}TransactionCallback"
internal fun AnnotatedValue.aidlType() =
AidlTypeSpec(Type(type.packageName, "Parcelable${type.simpleName}"))
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
index c791c9c..4d287b3 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -22,6 +22,8 @@
import androidx.privacysandbox.tools.core.model.Types
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.TypeName
/** Utility to generate [CodeBlock]s that convert values to/from their binder equivalent. */
abstract class BinderCodeConverter(private val api: ParsedApi) {
@@ -48,6 +50,21 @@
if (sandboxInterface != null) {
return convertToInterfaceModelCode(sandboxInterface, expression)
}
+ if (type.qualifiedName == List::class.qualifiedName) {
+ val convertToModelCodeBlock = convertToModelCode(type.typeParameters[0], "it")
+ return CodeBlock.of(
+ "%L%L.toList()",
+ expression,
+ // Only convert the list elements if necessary.
+ if (convertToModelCodeBlock == CodeBlock.of("it"))
+ CodeBlock.of("")
+ else
+ CodeBlock.of(".map { %L }", convertToModelCodeBlock)
+ )
+ }
+ if (type == Types.short) {
+ return CodeBlock.of("%L.toShort()", expression)
+ }
return CodeBlock.of(expression)
}
@@ -79,28 +96,70 @@
if (sandboxInterface != null) {
return convertToInterfaceBinderCode(sandboxInterface, expression)
}
+ if (type.qualifiedName == List::class.qualifiedName) {
+ val convertToBinderCodeBlock = convertToBinderCode(type.typeParameters[0], "it")
+ return CodeBlock.of(
+ "%L%L.%L()",
+ expression,
+ // Only convert the list elements if necessary.
+ if (convertToBinderCodeBlock == CodeBlock.of("it"))
+ CodeBlock.of("")
+ else
+ CodeBlock.of(".map { %L }", convertToBinderCodeBlock),
+ toBinderList(type.typeParameters[0])
+ )
+ }
+ if (type == Types.short) {
+ return CodeBlock.of("%L.toInt()", expression)
+ }
return CodeBlock.of(expression)
}
+ private fun toBinderList(type: Type) = when (type) {
+ Types.boolean -> "toBooleanArray"
+ Types.int -> "toIntArray"
+ Types.long -> "toLongArray"
+ Types.short -> "toIntArray"
+ Types.float -> "toFloatArray"
+ Types.double -> "toDoubleArray"
+ Types.char -> "toCharArray"
+ else -> "toTypedArray"
+ }
+
protected abstract fun convertToInterfaceBinderCode(
annotatedInterface: AnnotatedInterface,
expression: String
): CodeBlock
/** Convert the given model type declaration to its binder equivalent. */
- fun convertToBinderType(type: Type): ClassName {
+ fun convertToBinderType(type: Type): TypeName {
val value = api.valueMap[type]
if (value != null) {
return value.parcelableNameSpec()
}
val callback = api.callbackMap[type]
if (callback != null) {
- return callback.aidlType().innerType.poetSpec()
+ return callback.aidlType().innerType.poetTypeName()
}
val sandboxInterface = api.interfaceMap[type]
if (sandboxInterface != null) {
- return sandboxInterface.aidlType().innerType.poetSpec()
+ return sandboxInterface.aidlType().innerType.poetTypeName()
}
- return type.poetSpec()
+ if (type.qualifiedName == List::class.qualifiedName)
+ return convertToBinderListType(type)
+ return type.poetTypeName()
}
+
+ private fun convertToBinderListType(type: Type): TypeName =
+ when (type.typeParameters[0]) {
+ Types.boolean -> ClassName("kotlin", "BooleanArray")
+ Types.int -> ClassName("kotlin", "IntArray")
+ Types.long -> ClassName("kotlin", "LongArray")
+ Types.short -> ClassName("kotlin", "IntArray")
+ Types.float -> ClassName("kotlin", "FloatArray")
+ Types.double -> ClassName("kotlin", "DoubleArray")
+ Types.char -> ClassName("kotlin", "CharArray")
+ else -> ClassName("kotlin", "Array")
+ .parameterizedBy(convertToBinderType(type.typeParameters[0]))
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 97472c9..d224852 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -39,10 +39,10 @@
fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
val className = annotatedInterface.clientProxyNameSpec().simpleName
- val remoteBinderClassName = annotatedInterface.aidlType().innerType.poetSpec()
+ val remoteBinderClassName = annotatedInterface.aidlType().innerType.poetTypeName()
val classSpec = TypeSpec.classBuilder(className).build {
- addSuperinterface(annotatedInterface.type.poetSpec())
+ addSuperinterface(annotatedInterface.type.poetTypeName())
primaryConstructor(
listOf(
@@ -70,7 +70,7 @@
addModifiers(KModifier.OVERRIDE)
addModifiers(KModifier.SUSPEND)
addParameters(method.parameters.map { it.poetSpec() })
- returns(method.returnType.poetSpec())
+ returns(method.returnType.poetTypeName())
addCode {
addControlFlow("return %M", suspendCancellableCoroutineMethod) {
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index bb26bbd..8eb7838 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -27,17 +27,27 @@
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
/** [ParameterSpec] equivalent to this parameter. */
fun Parameter.poetSpec(): ParameterSpec {
- return ParameterSpec.builder(name, type.poetSpec()).build()
+ return ParameterSpec.builder(name, type.poetTypeName()).build()
}
-/** [TypeName] equivalent to this parameter. */
-fun Type.poetSpec() = ClassName(packageName, simpleName)
+/** [TypeName] equivalent to this type. */
+fun Type.poetTypeName(): TypeName {
+ val className = ClassName(packageName, simpleName)
+ if (typeParameters.isEmpty()) {
+ return className
+ }
+ return className.parameterizedBy(typeParameters.map { it.poetTypeName() })
+}
+
+/** [ClassName] equivalent to this type. */
+fun Type.poetClassName() = ClassName(packageName, simpleName)
fun AnnotatedValue.converterNameSpec() =
ClassName(type.packageName, "${type.simpleName}Converter")
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
index e94ab33..ec416a1 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
@@ -48,7 +48,7 @@
listOf(
PropertySpec.builder(
"delegate",
- annotatedInterface.type.poetSpec(),
+ annotatedInterface.type.poetTypeName(),
).addModifiers(KModifier.PUBLIC).build()
), KModifier.INTERNAL
)
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
index 0b5ced9..ad6f5f4 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
@@ -47,7 +47,7 @@
private fun generateToParcelable(value: AnnotatedValue) =
FunSpec.builder(value.toParcelableNameSpec().simpleName).build {
- addParameter("annotatedValue", value.type.poetSpec())
+ addParameter("annotatedValue", value.type.poetTypeName())
returns(value.parcelableNameSpec())
addStatement("val parcelable = %T()", value.parcelableNameSpec())
value.properties.map(::generateToParcelablePropertyConversion).forEach(::addCode)
@@ -68,10 +68,10 @@
private fun generateFromParcelable(value: AnnotatedValue) =
FunSpec.builder(value.fromParcelableNameSpec().simpleName).build {
addParameter("parcelable", value.parcelableNameSpec())
- returns(value.type.poetSpec())
+ returns(value.type.poetTypeName())
val parameters = value.properties.map(::generateFromParcelablePropertyConversion)
addStatement {
- add("val annotatedValue = %T(\n", value.type.poetSpec())
+ add("val annotatedValue = %T(\n", value.type.poetTypeName())
add(parameters.joinToCode(separator = ",\n"))
add(")")
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
index 9536140..79055e7 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
@@ -25,7 +25,7 @@
/**
* Generates a file that defines a previously declared SDK value.
*/
-class ValueFileGenerator() {
+class ValueFileGenerator {
fun generate(value: AnnotatedValue) =
FileSpec.builder(value.type.packageName, value.type.simpleName).build {
addCommonSettings()
@@ -33,10 +33,10 @@
}
private fun generateValue(value: AnnotatedValue) =
- TypeSpec.classBuilder(value.type.poetSpec()).build {
+ TypeSpec.classBuilder(value.type.poetClassName()).build {
addModifiers(KModifier.DATA)
primaryConstructor(value.properties.map {
- PropertySpec.builder(it.name, it.type.poetSpec())
+ PropertySpec.builder(it.name, it.type.poetTypeName())
.mutable(false)
.build()
})
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
index aeee60f..adf1862 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
@@ -24,4 +24,10 @@
val isList: Boolean = false
) {
override fun toString() = innerType.simpleName + if (isList) "[]" else ""
+
+ /** Returns a new type spec representing a list of this type. */
+ fun listSpec(): AidlTypeSpec {
+ require(!isList) { "Nested lists are not supported." }
+ return AidlTypeSpec(innerType, requiresImport, isList = true)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
index aeb2f9a..b41628c 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
@@ -59,11 +59,17 @@
isSuspend = true,
),
Method(
+ name = "suspendMethodWithLists",
+ parameters = listOf(Parameter("l", Types.list(Types.int))),
+ returnType = Types.list(Types.string),
+ isSuspend = true,
+ ),
+ Method(
name = "methodWithoutReturnValue",
parameters = listOf(),
returnType = Types.unit,
isSuspend = false,
- )
+ ),
)
)
)
@@ -73,6 +79,7 @@
assertThat(javaGeneratedSources.map { it.packageName to it.interfaceName })
.containsExactly(
"com.mysdk" to "IMySdk",
+ "com.mysdk" to "IListStringTransactionCallback",
"com.mysdk" to "IStringTransactionCallback",
"com.mysdk" to "IUnitTransactionCallback",
"com.mysdk" to "ICancellationSignal",
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
index 2b5ee15..48c98e3 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
@@ -47,7 +47,7 @@
Type(packageName = "com.mysdk", simpleName = "OuterValue"),
listOf(
ValueProperty("innerValue", innerValue.type),
- ValueProperty("anotherInnerValue", innerValue.type),
+ ValueProperty("innerValueList", Types.list(innerValue.type)),
)
)
@@ -71,6 +71,14 @@
isSuspend = true,
),
Method(
+ name = "suspendMethodWithListsOfValues",
+ parameters = listOf(
+ Parameter("inputValues", Types.list(outerValue.type))
+ ),
+ returnType = Types.list(outerValue.type),
+ isSuspend = true,
+ ),
+ Method(
name = "methodReceivingValue",
parameters = listOf(
Parameter(
@@ -95,6 +103,7 @@
"com.mysdk" to "ParcelableInnerValue",
"com.mysdk" to "IUnitTransactionCallback",
"com.mysdk" to "IOuterValueTransactionCallback",
+ "com.mysdk" to "IListOuterValueTransactionCallback",
"com.mysdk" to "ICancellationSignal",
"com.mysdk" to "PrivacySandboxThrowableParcel",
"com.mysdk" to "ParcelableStackFrame",
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl
new file mode 100644
index 0000000..c42fc32
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl
@@ -0,0 +1,10 @@
+package com.mysdk;
+
+import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
+
+oneway interface IListStringTransactionCallback {
+ void onCancellable(ICancellationSignal cancellationSignal);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
+ void onSuccess(in String[] result);
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
index 61d5f7f..1893e97 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
@@ -1,10 +1,12 @@
package com.mysdk;
+import com.mysdk.IListStringTransactionCallback;
import com.mysdk.IStringTransactionCallback;
import com.mysdk.IUnitTransactionCallback;
oneway interface IMySdk {
void methodWithoutReturnValue();
+ void suspendMethodWithLists(in int[] l, IListStringTransactionCallback transactionCallback);
void suspendMethodWithReturnValue(boolean a, int b, long c, float d, double e, char f, int g, IStringTransactionCallback transactionCallback);
void suspendMethodWithoutReturnValue(IUnitTransactionCallback transactionCallback);
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl
new file mode 100644
index 0000000..3ba8dc8
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ICancellationSignal;
+import com.mysdk.ParcelableOuterValue;
+import com.mysdk.PrivacySandboxThrowableParcel;
+
+oneway interface IListOuterValueTransactionCallback {
+ void onCancellable(ICancellationSignal cancellationSignal);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
+ void onSuccess(in ParcelableOuterValue[] result);
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
index 15b63d6..b4adfb2d 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
@@ -1,5 +1,6 @@
package com.mysdk;
+import com.mysdk.IListOuterValueTransactionCallback;
import com.mysdk.IOuterValueTransactionCallback;
import com.mysdk.IUnitTransactionCallback;
import com.mysdk.ParcelableOuterValue;
@@ -8,4 +9,5 @@
void methodReceivingValue(in ParcelableOuterValue value);
void suspendMethodReceivingValue(in ParcelableOuterValue inputValue, IUnitTransactionCallback transactionCallback);
void suspendMethodThatReturnsValue(IOuterValueTransactionCallback transactionCallback);
+ void suspendMethodWithListsOfValues(in ParcelableOuterValue[] inputValues, IListOuterValueTransactionCallback transactionCallback);
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
index 9627006..bd1851a 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
@@ -3,6 +3,6 @@
import com.mysdk.ParcelableInnerValue;
parcelable ParcelableOuterValue {
- ParcelableInnerValue anotherInnerValue;
ParcelableInnerValue innerValue;
+ ParcelableInnerValue[] innerValueList;
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index ffb490f..2969e3d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -155,11 +155,32 @@
val JAVA_CLASS = XClassName.get("java.lang", "Class")
}
-object GuavaBaseTypeNames {
+object GuavaTypeNames {
val OPTIONAL = XClassName.get("com.google.common.base", "Optional")
-}
-
-object GuavaCollectionTypeNames {
+ val IMMUTABLE_MULTIMAP_BUILDER = XClassName.get(
+ "com.google.common.collect",
+ "ImmutableMultimap",
+ "Builder"
+ )
+ val IMMUTABLE_SET_MULTIMAP = XClassName.get(
+ "com.google.common.collect",
+ "ImmutableSetMultimap"
+ )
+ val IMMUTABLE_SET_MULTIMAP_BUILDER = XClassName.get(
+ "com.google.common.collect",
+ "ImmutableSetMultimap",
+ "Builder"
+ )
+ val IMMUTABLE_LIST_MULTIMAP = XClassName.get(
+ "com.google.common.collect",
+ "ImmutableListMultimap"
+ )
+ val IMMUTABLE_LIST_MULTIMAP_BUILDER = XClassName.get(
+ "com.google.common.collect",
+ "ImmutableListMultimap",
+ "Builder"
+ )
+ val IMMUTABLE_MAP = XClassName.get("com.google.common.collect", "ImmutableMap")
val IMMUTABLE_LIST = XClassName.get("com.google.common.collect", "ImmutableList")
val IMMUTABLE_LIST_BUILDER = XClassName.get(
"com.google.common.collect",
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 69d5f4a..7846773 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
@@ -24,7 +24,7 @@
import androidx.room.ext.CollectionTypeNames.INT_SPARSE_ARRAY
import androidx.room.ext.CollectionTypeNames.LONG_SPARSE_ARRAY
import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.GuavaTypeNames
import androidx.room.ext.isByteBuffer
import androidx.room.ext.isEntityElement
import androidx.room.ext.isNotByte
@@ -121,7 +121,6 @@
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableMultimap
import com.google.common.collect.ImmutableSetMultimap
-import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -487,7 +486,7 @@
} else if (typeMirror.typeArguments.isEmpty()) {
val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
return SingleItemQueryResultAdapter(rowAdapter)
- } else if (typeMirror.rawType.asTypeName() == GuavaBaseTypeNames.OPTIONAL) {
+ } else if (typeMirror.rawType.asTypeName() == GuavaTypeNames.OPTIONAL) {
// Handle Guava Optional by unpacking its generic type argument and adapting that.
// The Optional adapter will reappend the Optional type.
val typeArg = typeMirror.typeArguments.first()
@@ -559,9 +558,9 @@
}
val immutableClassName = if (typeMirror.isTypeOf(ImmutableListMultimap::class)) {
- ClassName.get(ImmutableListMultimap::class.java)
+ GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP
} else if (typeMirror.isTypeOf(ImmutableSetMultimap::class)) {
- ClassName.get(ImmutableSetMultimap::class.java)
+ GuavaTypeNames.IMMUTABLE_SET_MULTIMAP
} else {
// Return type is base class ImmutableMultimap which is not recommended.
context.logger.e(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
index 5b991a1..d57eb7d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -16,14 +16,13 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
class GuavaImmutableMultimapQueryResultAdapter(
context: Context,
@@ -32,12 +31,11 @@
override val valueTypeArg: XType,
private val keyRowAdapter: QueryMappedRowAdapter,
private val valueRowAdapter: QueryMappedRowAdapter,
- private val immutableClassName: ClassName,
+ private val immutableClassName: XClassName,
) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
- private val mapType = ParameterizedTypeName.get(
- immutableClassName,
- keyTypeArg.typeName,
- valueTypeArg.typeName
+ private val mapType = immutableClassName.parametrizedBy(
+ keyTypeArg.asTypeName(),
+ valueTypeArg.asTypeName()
)
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
@@ -66,18 +64,41 @@
it.onCursorReady(cursorVarName = cursorVarName, scope = scope)
}
}
- addStatement(
- "final $T.Builder<$T, $T> $L = $T.builder()",
- immutableClassName,
- keyTypeArg.typeName,
- valueTypeArg.typeName,
- mapVarName,
- immutableClassName
+
+ val builderClassName = when (immutableClassName) {
+ GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP ->
+ GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP_BUILDER
+
+ GuavaTypeNames.IMMUTABLE_SET_MULTIMAP ->
+ GuavaTypeNames.IMMUTABLE_SET_MULTIMAP_BUILDER
+
+ else ->
+ // Return type is base class ImmutableMultimap, need the case handled here,
+ // but won't actually get here in the code if this is the case as we will
+ // do an early return in TypeAdapterStore.kt.
+ GuavaTypeNames.IMMUTABLE_MULTIMAP_BUILDER
+ }
+
+ addLocalVariable(
+ name = mapVarName,
+ typeName = builderClassName.parametrizedBy(
+ keyTypeArg.asTypeName(),
+ valueTypeArg.asTypeName()
+ ),
+ assignExpr = XCodeBlock.of(
+ language = language,
+ format = "%T.builder()",
+ immutableClassName
+ )
)
+
val tmpKeyVarName = scope.getTmpVar("_key")
val tmpValueVarName = scope.getTmpVar("_value")
- beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
- addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+ beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+ addLocalVariable(
+ name = tmpKeyVarName,
+ typeName = keyTypeArg.asTypeName()
+ )
keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
// Iterate over all matched fields to check if all are null. If so, we continue in
@@ -91,16 +112,27 @@
indexVars = valueIndexVars
)
// Perform column null check
- beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
+ beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
addStatement("continue")
}.endControlFlow()
- addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+ addLocalVariable(
+ name = tmpValueVarName,
+ typeName = valueTypeArg.asTypeName()
+ )
valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
- addStatement("$L.put($L, $L)", mapVarName, tmpKeyVarName, tmpValueVarName)
+ addStatement("%L.put(%L, %L)", mapVarName, tmpKeyVarName, tmpValueVarName)
}
endControlFlow()
- addStatement("final $T $L = $L.build()", mapType, outVarName, mapVarName)
+ addLocalVariable(
+ name = outVarName,
+ typeName = mapType,
+ assignExpr = XCodeBlock.of(
+ language = language,
+ format = "%L.build()",
+ mapVarName
+ )
+ )
}
}
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
index fe94c86..26fba57 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
@@ -18,7 +18,7 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.GuavaTypeNames
import androidx.room.solver.CodeGenScope
/**
@@ -39,13 +39,13 @@
resultAdapter.convert(valueVarName, cursorVarName, scope)
addLocalVariable(
name = outVarName,
- typeName = GuavaBaseTypeNames.OPTIONAL.parametrizedBy(
+ typeName = GuavaTypeNames.OPTIONAL.parametrizedBy(
typeArg.asTypeName()
),
assignExpr = XCodeBlock.of(
language = language,
format = "%T.fromNullable(%L)",
- GuavaBaseTypeNames.OPTIONAL,
+ GuavaTypeNames.OPTIONAL,
valueVarName
)
)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
index ef1068e..5f5e27b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
@@ -19,7 +19,7 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaCollectionTypeNames
+import androidx.room.ext.GuavaTypeNames
import androidx.room.solver.CodeGenScope
class ImmutableListQueryResultAdapter(
@@ -29,10 +29,10 @@
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
scope.builder.apply {
rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
- val collectionType = GuavaCollectionTypeNames.IMMUTABLE_LIST.parametrizedBy(
+ val collectionType = GuavaTypeNames.IMMUTABLE_LIST.parametrizedBy(
typeArg.asTypeName()
)
- val immutableListBuilderType = GuavaCollectionTypeNames
+ val immutableListBuilderType = GuavaTypeNames
.IMMUTABLE_LIST_BUILDER.parametrizedBy(typeArg.asTypeName())
val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
addLocalVariable(
@@ -40,7 +40,7 @@
typeName = immutableListBuilderType,
assignExpr = XCodeBlock.ofNewInstance(
language = language,
- GuavaCollectionTypeNames.IMMUTABLE_LIST_BUILDER
+ GuavaTypeNames.IMMUTABLE_LIST_BUILDER
)
)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
index 9bddcae..3f99950 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
@@ -16,15 +16,12 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.solver.CodeGenScope
-import com.google.common.collect.ImmutableMap
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
class ImmutableMapQueryResultAdapter(
context: Context,
@@ -34,19 +31,21 @@
private val resultAdapter: QueryResultAdapter
) : MultimapQueryResultAdapter(context, parsedQuery, resultAdapter.rowAdapters) {
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
+ scope.builder.apply {
val mapVarName = scope.getTmpVar("_mapResult")
resultAdapter.convert(mapVarName, cursorVarName, scope)
- addStatement(
- "final $T $L = $T.copyOf($L)",
- ParameterizedTypeName.get(
- ClassName.get(ImmutableMap::class.java),
- keyTypeArg.typeName,
- valueTypeArg.typeName
+ addLocalVariable(
+ name = outVarName,
+ typeName = GuavaTypeNames.IMMUTABLE_MAP.parametrizedBy(
+ keyTypeArg.asTypeName(),
+ valueTypeArg.asTypeName()
),
- outVarName,
- ClassName.get(ImmutableMap::class.java),
- mapVarName
+ assignExpr = XCodeBlock.of(
+ language = language,
+ format = "%T.copyOf(%L)",
+ GuavaTypeNames.IMMUTABLE_MAP,
+ mapVarName
+ ),
)
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index b921ed6..247f882 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -1368,6 +1368,87 @@
}
@Test
+ fun queryResultAdapter_guavaImmutableMultimap() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+ fun getArtistWithSongs(): com.google.common.collect.ImmutableSetMultimap<Artist, Song>
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+ fun getArtistWithSongIds(): com.google.common.collect.ImmutableListMultimap<Artist, Song>
+ }
+
+ @Entity
+ data class Artist(
+ @PrimaryKey
+ val artistId: String
+ )
+
+ @Entity
+ data class Song(
+ @PrimaryKey
+ val songId: String,
+ val artistKey: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun queryResultAdapter_guavaImmutableMap() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+ abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+ }
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+ fun getSongsWithArtist(): com.google.common.collect.ImmutableMap<Song, Artist>
+ }
+
+ @Entity
+ data class Artist(
+ @PrimaryKey
+ val artistId: String
+ )
+
+ @Entity
+ data class Song(
+ @PrimaryKey
+ val songId: String,
+ val artistKey: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
fun queryResultAdapter_map_ambiguousIndexAdapter() {
val testName = object {}.javaClass.enclosingMethod!!.name
val src = Source.kotlin(
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt
new file mode 100644
index 0000000..2e4041b
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt
@@ -0,0 +1,68 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import com.google.common.collect.ImmutableMap
+import java.lang.Class
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun getSongsWithArtist(): ImmutableMap<Song, Artist> {
+ val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _mapResult: MutableMap<Song, Artist> = LinkedHashMap<Song, Artist>()
+ while (_cursor.moveToNext()) {
+ val _key: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _key = Song(_tmpSongId,_tmpArtistKey)
+ if (_cursor.isNull(_cursorIndexOfArtistId)) {
+ error("Missing value for a key.")
+ }
+ val _value: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _value = Artist(_tmpArtistId)
+ if (!_mapResult.containsKey(_key)) {
+ _mapResult.put(_key, _value)
+ }
+ }
+ val _result: ImmutableMap<Song, Artist> = ImmutableMap.copyOf(_mapResult)
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt
new file mode 100644
index 0000000..7da4276
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt
@@ -0,0 +1,99 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import com.google.common.collect.ImmutableListMultimap
+import com.google.common.collect.ImmutableSetMultimap
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION"])
+public class MyDao_Impl(
+ __db: RoomDatabase,
+) : MyDao {
+ private val __db: RoomDatabase
+ init {
+ this.__db = __db
+ }
+
+ public override fun getArtistWithSongs(): ImmutableSetMultimap<Artist, Song> {
+ val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _mapBuilder: ImmutableSetMultimap.Builder<Artist, Song> = ImmutableSetMultimap.builder()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _key = Artist(_tmpArtistId)
+ if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+ continue
+ }
+ val _value: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _value = Song(_tmpSongId,_tmpArtistKey)
+ _mapBuilder.put(_key, _value)
+ }
+ val _result: ImmutableSetMultimap<Artist, Song> = _mapBuilder.build()
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun getArtistWithSongIds(): ImmutableListMultimap<Artist, Song> {
+ val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+ val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+ val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+ val _mapBuilder: ImmutableListMultimap.Builder<Artist, Song> = ImmutableListMultimap.builder()
+ while (_cursor.moveToNext()) {
+ val _key: Artist
+ val _tmpArtistId: String
+ _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+ _key = Artist(_tmpArtistId)
+ if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+ continue
+ }
+ val _value: Song
+ val _tmpSongId: String
+ _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+ val _tmpArtistKey: String
+ _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+ _value = Song(_tmpSongId,_tmpArtistKey)
+ _mapBuilder.put(_key, _value)
+ }
+ val _result: ImmutableListMultimap<Artist, Song> = _mapBuilder.build()
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index afcd43e..8b90e5d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -895,6 +895,11 @@
includeProject(":viewpager2:integration-tests:targetsdk-tests", [BuildType.MAIN])
includeProject(":viewpager2:viewpager2", [BuildType.MAIN])
includeProject(":viewpager:viewpager", [BuildType.MAIN])
+includeProject(":wear:protolayout:protolayout", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-expression", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-expression-pipeline", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-proto", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-renderer", [BuildType.WEAR])
includeProject(":wear:wear", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":wear:benchmark:integration-tests:macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index c88a125..15d7d5f 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -35,6 +35,7 @@
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
+import org.junit.Ignore;
import org.junit.Test;
import java.util.HashSet;
@@ -702,6 +703,7 @@
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
}
+ @Ignore // b/260235822
@Test
public void testSetGestureMargins() {
launchTestActivity(PinchTestActivity.class);
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
index a0397ce..0687e7c 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
@@ -25,7 +25,7 @@
// Concept of version useful e.g. for human-readable error messages, and stable once released.
// Does not replace the need for a binary verification mechanism (e.g. checksum check).
// TODO: populate using CMake
-#define VERSION "1.0.0-alpha07"
+#define VERSION "1.0.0-alpha08"
namespace tracing_perfetto {
void RegisterWithPerfetto() {
diff --git a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
index fb25e2e..d789c54 100644
--- a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
+++ b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
@@ -30,7 +30,7 @@
init {
PerfettoNative.loadLib()
}
- const val libraryVersion = "1.0.0-alpha07" // TODO: get using reflection
+ const val libraryVersion = "1.0.0-alpha08" // TODO: get using reflection
}
@Test
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index 00e6c5a..307966f 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -25,12 +25,12 @@
// TODO(224510255): load from a file produced at build time
object Metadata {
- const val version = "1.0.0-alpha07"
+ const val version = "1.0.0-alpha08"
val checksums = mapOf(
- "arm64-v8a" to "3d9fa02a04459e3480f28ae7f3a8ced30f181f02a0e6bc6caf85704faea84857",
- "armeabi-v7a" to "9eb7bf76a94fce79e682204c9e702c872d15c85f2ebd48de61aa541e7e6de034",
- "x86" to "6eb72683bf58eeb0175f094d88bc0e00744b1a593dc7eaac9ee15168b1ab9234",
- "x86_64" to "eabf725464c24c9709d8e2ea073233e5712949bb65aac5c4972528d813b55c74",
+ "arm64-v8a" to "3accfdd32b900833af761bb5cb4576b9d0bf75c49a5ed98b84ea1bc1447d82e5",
+ "armeabi-v7a" to "1fbe02f3cc570677ec55fb1e74e2719e73d6e5914e4f13886f915c9d588b939c",
+ "x86" to "0b6a550f0f95700ab4a8bbbbd04119aa3c4a6dcce3ace6599bb3d4b908b42258",
+ "x86_64" to "deb5b286f109c4bb95a33f168a642c290284aa805bef1f8cac4734b9adb922ab",
)
}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index a0f499c..c5dfada 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -18,11 +18,17 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@@ -40,9 +46,41 @@
import androidx.tv.material.carousel.Carousel
import androidx.tv.material.carousel.CarouselItem
+@Composable
+fun FeaturedCarouselContent() {
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ item { SampleLazyRow() }
+ item {
+ Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
+ Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ repeat(3) {
+ Box(modifier = Modifier
+ .background(Color.Magenta.copy(alpha = 0.3f))
+ .width(50.dp)
+ .height(50.dp)
+ .drawBorderOnFocus()
+ )
+ }
+ }
+ FeaturedCarousel()
+ }
+ }
+ items(2) { SampleLazyRow() }
+ }
+}
+
+@Composable
+fun Modifier.drawBorderOnFocus(borderColor: Color = Color.White): Modifier {
+ var isFocused by remember { mutableStateOf(false) }
+ return this
+ .border(5.dp, borderColor.copy(alpha = if (isFocused) 1f else 0.2f))
+ .onFocusChanged { isFocused = it.isFocused }
+ .focusable()
+}
+
@OptIn(ExperimentalTvMaterialApi::class)
@Composable
-fun FeaturedCarousel() {
+internal fun FeaturedCarousel() {
val backgrounds = listOf(
Color.Red.copy(alpha = 0.3f),
Color.Yellow.copy(alpha = 0.3f),
@@ -77,7 +115,7 @@
modifier = Modifier
.fillMaxSize()
.padding(40.dp),
- contentAlignment = Alignment.CenterStart
+ contentAlignment = Alignment.BottomStart
) {
var isFocused by remember { mutableStateOf(false) }
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
index bbf0111..f28379e 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
@@ -17,19 +17,12 @@
package androidx.tv.tvmaterial.samples
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.lazy.list.TvLazyColumn
@@ -42,29 +35,25 @@
fun LazyRowsAndColumns() {
TvLazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
repeat((0 until rowsCount).count()) {
- item { LazyRow() }
+ item { SampleLazyRow() }
}
}
}
@Composable
-private fun LazyRow() {
+fun SampleLazyRow() {
val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
val backgroundColors = (0 until columnsCount).map { colors.random() }
TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
backgroundColors.forEach { backgroundColor ->
item {
- var isFocused by remember { mutableStateOf(false) }
-
Box(
modifier = Modifier
.background(backgroundColor.copy(alpha = 0.3f))
.width(200.dp)
.height(150.dp)
- .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.2f))
- .onFocusChanged { isFocused = it.isFocused }
- .focusable()
+ .drawBorderOnFocus()
)
}
}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
index 6ed68d5..6648a0a 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -38,7 +38,7 @@
enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
LazyRowsAndColumns("Lazy Rows and Columns", { LazyRowsAndColumns() }),
- FeaturedCarousel("Featured Carousel", { FeaturedCarousel() }),
+ FeaturedCarousel("Featured Carousel", { FeaturedCarouselContent() }),
ImmersiveList("Immersive List", { SampleImmersiveList() }),
}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index c4933a3..158ea43 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -437,6 +438,64 @@
}
@Test
+ fun carousel_manualScrolling_withFocusableItemsOnTop() {
+ rule.setContent {
+ Column {
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ repeat(3) {
+ SampleButton("Row-button-${it + 1}")
+ }
+ }
+ SampleCarousel { index ->
+ SampleButton("Button-${index + 1}")
+ }
+ }
+ }
+
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag("pager")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+
+ // trigger recomposition on requesting focus
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+
+ // Check that slide 1 is in view and button 1 has focus
+ rule.onNodeWithText("Button-1").assertIsDisplayed()
+ rule.onNodeWithText("Button-1").assertIsFocused()
+
+ // press dpad right to scroll to next slide
+ performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+ // Wait for slide to load
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeBy(animationTime, false)
+ rule.waitForIdle()
+
+ // Check that slide 2 is in view and button 2 has focus
+ rule.onNodeWithText("Button-2").assertIsDisplayed()
+ // TODO: Fix button 2 isn't gaining focus
+ // rule.onNodeWithText("Button-2").assertIsFocused()
+
+ // Check if the first focusable element in parent has focus
+ rule.onNodeWithText("Row-button-1").assertIsNotFocused()
+
+ // press dpad left to scroll to previous slide
+ performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+ // Wait for slide to load
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeBy(animationTime, false)
+ rule.waitForIdle()
+
+ // Check that slide 1 is in view and button 1 has focus
+ rule.onNodeWithText("Button-1").assertIsDisplayed()
+ rule.onNodeWithText("Button-1").assertIsFocused()
+ }
+
+ @Test
fun carousel_manualScrolling_ltr() {
rule.setContent {
SampleCarousel { index ->
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index 2b37161..6200545 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -17,6 +17,7 @@
package androidx.tv.material.carousel
import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
@@ -52,7 +53,6 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
@@ -102,7 +102,7 @@
var focusState: FocusState? by remember { mutableStateOf(null) }
val focusManager = LocalFocusManager.current
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
- val focusRequester = remember { FocusRequester() }
+ val carouselOuterBoxFocusRequester = remember { FocusRequester() }
var isAutoScrollActive by remember { mutableStateOf(false) }
AutoScrollSideEffect(
@@ -112,76 +112,40 @@
focusState,
onAutoScrollChange = { isAutoScrollActive = it })
Box(modifier = modifier
- .focusRequester(focusRequester)
+ .focusRequester(carouselOuterBoxFocusRequester)
.onFocusChanged {
focusState = it
if (it.isFocused && isAutoScrollActive) {
focusManager.moveFocus(FocusDirection.Enter)
}
}
- .focusProperties {
- exit = {
- val showPreviousSlideAndGetFocusRequester = {
- if (carouselState
- .isFirstSlide()
- .not()
- ) {
- carouselState.moveToPreviousSlide(slideCount)
- focusRequester
- } else {
- FocusRequester.Default
- }
- }
- val showNextSlideAndGetFocusRequester = {
- if (carouselState
- .isLastSlide(slideCount)
- .not()
- ) {
- carouselState.moveToNextSlide(slideCount)
- focusRequester
- } else {
- FocusRequester.Default
- }
- }
- when (it) {
- FocusDirection.Left -> {
- if (isLtr) {
- showPreviousSlideAndGetFocusRequester()
- } else {
- showNextSlideAndGetFocusRequester()
- }
- }
- FocusDirection.Right -> {
- if (isLtr) {
- showNextSlideAndGetFocusRequester()
- } else {
- showPreviousSlideAndGetFocusRequester()
- }
- }
- else -> FocusRequester.Default
- }
- }
- }
+ .manualScrolling(carouselState, slideCount, isLtr)
.focusable()) {
AnimatedContent(
targetState = carouselState.slideIndex,
transitionSpec = { enterTransition.with(exitTransition) }
) {
- Box(
- modifier = Modifier
- .onPlaced {
- if (isAutoScrollActive.not()) {
- focusManager.moveFocus(FocusDirection.Enter)
- }
+ LaunchedEffect(Unit) {
+ this@AnimatedContent.onAnimationCompletion {
+ if (isAutoScrollActive.not()) {
+ carouselOuterBoxFocusRequester.requestFocus()
+ focusManager.moveFocus(FocusDirection.Enter)
}
- ) {
- content.invoke(it)
+ }
}
+ content.invoke(it)
}
this.carouselIndicator()
}
}
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalAnimationApi::class)
+private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
+ snapshotFlow { transition.currentState == transition.targetState }.first { it }
+ action.invoke()
+}
+
@OptIn(ExperimentalTvMaterialApi::class)
@Composable
private fun AutoScrollSideEffect(
@@ -213,6 +177,53 @@
onAutoScrollChange(doAutoScroll)
}
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalTvMaterialApi::class, ExperimentalComposeUiApi::class)
+private fun Modifier.manualScrolling(
+ carouselState: CarouselState,
+ slideCount: Int,
+ isLtr: Boolean
+): Modifier =
+ this.focusProperties {
+ exit = {
+ val showPreviousSlideAndGetFocusRequester = {
+ if (carouselState.isFirstSlide().not()) {
+ carouselState.moveToPreviousSlide(slideCount)
+ FocusRequester.Cancel
+ } else {
+ FocusRequester.Default
+ }
+ }
+ val showNextSlideAndGetFocusRequester = {
+ if (carouselState.isLastSlide(slideCount).not()) {
+ carouselState.moveToNextSlide(slideCount)
+ FocusRequester.Cancel
+ } else {
+ FocusRequester.Default
+ }
+ }
+ when (it) {
+ FocusDirection.Left -> {
+ if (isLtr) {
+ showPreviousSlideAndGetFocusRequester()
+ } else {
+ showNextSlideAndGetFocusRequester()
+ }
+ }
+
+ FocusDirection.Right -> {
+ if (isLtr) {
+ showNextSlideAndGetFocusRequester()
+ } else {
+ showPreviousSlideAndGetFocusRequester()
+ }
+ }
+
+ else -> FocusRequester.Default
+ }
+ }
+ }
+
@OptIn(ExperimentalTvMaterialApi::class)
@Composable
private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 9fa9d6e..6f34d1e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -81,16 +81,17 @@
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(overlayVisible) {
- snapshotFlow { overlayVisible.isIdle && overlayVisible.currentState }.first { it }
- // slide has loaded completely.
- if (focusState?.isFocused == true) {
- // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
- // item is within an animation, bringIntoView scrolls excessively and loses focus.
- // b/241591211
- // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
- // completed animating into position.
- bringIntoViewRequester.bringIntoView()
- focusManager.moveFocus(FocusDirection.Enter)
+ overlayVisible.onAnimationCompletion {
+ // slide has loaded completely.
+ if (focusState?.isFocused == true) {
+ // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
+ // item is within an animation, bringIntoView scrolls excessively and loses focus.
+ // b/241591211
+ // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
+ // completed animating into position.
+ bringIntoViewRequester.bringIntoView()
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
}
}
@@ -119,7 +120,9 @@
modifier = Modifier
.align(Alignment.BottomStart)
.onFocusChanged {
- if (it.isFocused) { focusManager.moveFocus(FocusDirection.Enter) }
+ if (it.isFocused) {
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
}
.focusable(),
visibleState = overlayVisible,
@@ -131,6 +134,13 @@
}
}
+private suspend fun MutableTransitionState<Boolean>.onAnimationCompletion(
+ action: suspend () -> Unit
+) {
+ snapshotFlow { isIdle && currentState }.first { it }
+ action.invoke()
+}
+
@ExperimentalTvMaterialApi
object CarouselItemDefaults {
/**
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 870a8dc..ac201f8 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -43,6 +43,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -179,6 +180,7 @@
assertThat(state.selectedOption).isEqualTo(numberOfOptions - 1)
}
+ @SdkSuppress(minSdkVersion = 29) // b/260234023
@Test
fun uses_positive_separation_correctly() =
uses_separation_correctly(1)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index e681cd5..529cdac 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -42,6 +42,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Before
@@ -203,6 +204,7 @@
)
}
+ @SdkSuppress(minSdkVersion = 29) // b/260235088
@Test
fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPadding() {
scrollableScalingLazyColumnPositionAndSize(
diff --git a/wear/protolayout/OWNERS b/wear/protolayout/OWNERS
new file mode 100644
index 0000000..bc950d4
--- /dev/null
+++ b/wear/protolayout/OWNERS
@@ -0,0 +1,2 @@
+msab@google.com
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt b/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
new file mode 100644
index 0000000..6bb2d3b
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * 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)
+}
+
+android {
+ namespace "androidx.wear.protolayout.expression.pipeline"
+}
+
+androidx {
+ name = "ProtoLayout Dynamic Expression Evaluation Pipeline"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+ inceptionYear = "2022"
+ description = "Evaluate dynamic expressions."
+}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/api/res-current.txt b/wear/protolayout/protolayout-expression/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/res-current.txt
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
new file mode 100644
index 0000000..6e5826a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * 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)
+}
+
+android {
+ namespace "androidx.wear.protolayout.expression"
+}
+
+androidx {
+ name = "ProtoLayout Dynamic Expression"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+ inceptionYear = "2022"
+ description = "Create dynamic expressions (for late evaluation by a remote evaluator)."
+}
diff --git a/wear/protolayout/protolayout-proto/api/current.txt b/wear/protolayout/protolayout-proto/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/api/restricted_current.txt b/wear/protolayout/protolayout-proto/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/build.gradle b/wear/protolayout/protolayout-proto/build.gradle
new file mode 100644
index 0000000..42da9e5
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * 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("java-library")
+}
+
+dependencies {
+ annotationProcessor(libs.nullaway)
+ implementation("androidx.annotation:annotation:1.1.0")
+}
+
+androidx {
+ name = "Wear ProtoLayout Proto"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+ inceptionYear = "2022"
+ description = "Jarjar the generated proto and proto-lite dependency for use by wear-protolayout"
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java b/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java
new file mode 100644
index 0000000..64a3b00
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.wear.protolayout.proto;
+
+import androidx.annotation.RestrictTo;
diff --git a/wear/protolayout/protolayout-renderer/api/current.txt b/wear/protolayout/protolayout-renderer/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/api/res-current.txt b/wear/protolayout/protolayout-renderer/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/res-current.txt
diff --git a/wear/protolayout/protolayout-renderer/api/restricted_current.txt b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/build.gradle b/wear/protolayout/protolayout-renderer/build.gradle
new file mode 100644
index 0000000..a9bc2f2
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * 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)
+}
+
+android {
+ namespace "androidx.wear.protolayout.renderer"
+}
+
+androidx {
+ name = "ProtoLayout Renderer"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+ inceptionYear = "2022"
+ description = "Render ProtoLayouts to an Android surface"
+}
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/api/res-current.txt b/wear/protolayout/protolayout/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout/api/res-current.txt
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
new file mode 100644
index 0000000..74f7467
--- /dev/null
+++ b/wear/protolayout/protolayout/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * 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)
+}
+
+android {
+ namespace "androidx.wear.protolayout"
+}
+
+androidx {
+ name = "ProtoLayout"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+ inceptionYear = "2022"
+ description = "Create layouts that can be rendered by a remote host."
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java
new file mode 100644
index 0000000..4496dbc
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Allows creating layouts and expressions that can be rendered or evaluated at a remote host.
+ */
+package androidx.wear.protolayout;
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
index 1f81fc6..d7c9869 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
import java.util.List;
@@ -35,20 +36,11 @@
@VersionedParcelize
public class ComplicationsUserStyleSettingWireFormat extends UserStyleSettingWireFormat {
- ComplicationsUserStyleSettingWireFormat() {
- }
+ @Nullable
+ @ParcelField(104)
+ public List<CharSequence> mPerOptionScreenReaderNames;
- /** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
- @Deprecated
- public ComplicationsUserStyleSettingWireFormat(
- @NonNull String id,
- @NonNull CharSequence displayName,
- @NonNull CharSequence description,
- @Nullable Icon icon,
- @NonNull List<OptionWireFormat> options,
- int defaultOptionIndex,
- @NonNull List<Integer> affectsLayers) {
- super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers);
+ ComplicationsUserStyleSettingWireFormat() {
}
public ComplicationsUserStyleSettingWireFormat(
@@ -60,8 +52,10 @@
int defaultOptionIndex,
@NonNull List<Integer> affectsLayers,
@Nullable Bundle onWatchFaceEditorBundle,
- @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles) {
+ @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles,
+ @Nullable List<CharSequence> perOptionScreenReaderNames) {
super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers,
onWatchFaceEditorBundle, perOptionOnWatchFaceEditorBundles);
+ mPerOptionScreenReaderNames = perOptionScreenReaderNames;
}
}
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
index 0c50c6c..7669556 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
import java.util.List;
@@ -35,20 +36,11 @@
@VersionedParcelize
public class ListUserStyleSettingWireFormat extends UserStyleSettingWireFormat {
- ListUserStyleSettingWireFormat() {}
+ @Nullable
+ @ParcelField(104)
+ public List<CharSequence> mPerOptionScreenReaderNames;
- /** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
- @Deprecated
- public ListUserStyleSettingWireFormat(
- @NonNull String id,
- @NonNull CharSequence displayName,
- @NonNull CharSequence description,
- @Nullable Icon icon,
- @NonNull List<OptionWireFormat> options,
- int defaultOptionIndex,
- @NonNull List<Integer> affectsLayers) {
- super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers);
- }
+ ListUserStyleSettingWireFormat() {}
public ListUserStyleSettingWireFormat(
@NonNull String id,
@@ -59,8 +51,10 @@
int defaultOptionIndex,
@NonNull List<Integer> affectsLayers,
@Nullable Bundle onWatchFaceEditorBundle,
- @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles) {
+ @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles,
+ @Nullable List<CharSequence> perOptionScreenReaderNames) {
super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers,
onWatchFaceEditorBundle, perOptionOnWatchFaceEditorBundles);
+ mPerOptionScreenReaderNames = perOptionScreenReaderNames;
}
}
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
index b101976..13609b0 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
@@ -111,6 +111,8 @@
@ParcelField(103)
public List<Bundle> mPerOptionOnWatchFaceEditorBundles = new ArrayList<>();
+ // Field 104 is reserved.
+
UserStyleSettingWireFormat() {}
/** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index ae4c81f..f8818fe 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -148,9 +148,10 @@
private typealias WireComplicationProviderInfo =
android.support.wearable.complications.ComplicationProviderInfo
-internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
-internal val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
-internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", icon = null)
+internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
+internal val greenStyleOption =
+ ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
+internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", "Blue", icon = null)
internal val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
internal val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
@@ -161,9 +162,12 @@
listOf(WatchFaceLayer.BASE)
)
-internal val classicStyleOption = ListOption(Option.Id("classic_style"), "Classic", icon = null)
-internal val modernStyleOption = ListOption(Option.Id("modern_style"), "Modern", icon = null)
-internal val gothicStyleOption = ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+internal val classicStyleOption =
+ ListOption(Option.Id("classic_style"), "Classic", "Classic", icon = null)
+internal val modernStyleOption =
+ ListOption(Option.Id("modern_style"), "Modern", "Modern", icon = null)
+internal val gothicStyleOption =
+ ListOption(Option.Id("gothic_style"), "Gothic", "Gothic", icon = null)
internal val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
internal val watchHandStyleSetting = UserStyleSetting.ListUserStyleSetting(
@@ -206,6 +210,7 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
"Left And Right",
+ "Show left and right complications",
null,
// An empty list means use the initial config.
emptyList()
@@ -214,6 +219,7 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_COMPLICATION"),
"Left",
+ "Show left complication only",
null,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
@@ -225,6 +231,7 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("RIGHT_COMPLICATION"),
"Right",
+ "Show right complication only",
null,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
@@ -2413,8 +2420,8 @@
@Test
public fun cantAssignUnrelatedUserStyle() {
- val redOption = ListOption(Option.Id("red"), "Red", icon = null)
- val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
+ val redOption = ListOption(Option.Id("red"), "Red", "Red", icon = null)
+ val greenOption = ListOption(Option.Id("green"), "Green", "Green", icon = null)
val colorStyleList = listOf(redOption, greenOption)
val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_id"),
@@ -2446,8 +2453,8 @@
@Test
public fun cantAssignUnrelatedUserStyle_compareAndSet() {
- val redOption = ListOption(Option.Id("red"), "Red", icon = null)
- val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
+ val redOption = ListOption(Option.Id("red"), "Red", "Red", icon = null)
+ val greenOption = ListOption(Option.Id("green"), "Green", "Green", icon = null)
val colorStyleList = listOf(redOption, greenOption)
val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_id"),
diff --git a/wear/watchface/watchface-style/api/current.txt b/wear/watchface/watchface-style/api/current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/current.txt
+++ b/wear/watchface/watchface-style/api/current.txt
@@ -157,15 +157,19 @@
}
public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
@@ -214,14 +218,19 @@
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
diff --git a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
@@ -157,15 +157,19 @@
}
public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
@@ -214,14 +218,19 @@
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -157,15 +157,19 @@
}
public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
@@ -214,14 +218,19 @@
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+ ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
+ method public CharSequence? getScreenReaderName();
method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
+ property public final CharSequence? screenReaderName;
property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
}
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
index 07829db..73aff62 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
@@ -120,18 +120,21 @@
val classicStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("classic_style"),
"Classic",
+ "Classic screen reader name",
testIcon
)
val modernStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("modern_style"),
"Modern",
+ "Modern screen reader name",
testIcon
)
val gothicStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("gothic_style"),
"Gothic",
+ "Gothic screen reader name",
testIcon
)
@@ -201,12 +204,14 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
UserStyleSetting.Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
"Both",
+ "Both screen reader name",
testIcon,
listOf()
),
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
UserStyleSetting.Option.Id("NO_COMPLICATIONS"),
"None",
+ "None screen reader name",
testIcon,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -222,6 +227,7 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
UserStyleSetting.Option.Id("LEFT_COMPLICATION"),
"Left",
+ "Left screen reader name",
testIcon,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -233,6 +239,7 @@
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
UserStyleSetting.Option.Id("RIGHT_COMPLICATION"),
"Right",
+ "Right screen reader name",
testIcon,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
index 024640e..e8bff3b 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
@@ -68,12 +68,14 @@
val option00 = (setting0.options[0] as ListOption)
assertThat(option00.id).isEqualTo(UserStyleSetting.Option.Id("red"))
assertThat(option00.displayName).isEqualTo("Red Style")
+ assertThat(option00.screenReaderName).isEqualTo("Red watch face style")
assertThat(option00.icon!!.resId).isEqualTo(R.drawable.red_icon)
assertThat(option00.childSettings).isEmpty()
assertThat(option00.watchFaceEditorData!!.icon!!.resId).isEqualTo(R.drawable.red_icon_wf)
val option01 = (setting0.options[1] as ListOption)
assertThat(option01.id).isEqualTo(UserStyleSetting.Option.Id("green"))
assertThat(option01.displayName).isEqualTo("Green Style")
+ assertThat(option01.screenReaderName).isEqualTo("Green Style")
assertThat(option01.icon!!.resId).isEqualTo(R.drawable.green_icon)
assertThat(option01.childSettings).isEmpty()
assertThat(option01.watchFaceEditorData!!.icon!!.resId).isEqualTo(R.drawable.green_icon_wf)
@@ -92,12 +94,14 @@
val option10 = (setting1.options[0] as ListOption)
assertThat(option10.id).isEqualTo(UserStyleSetting.Option.Id("foo"))
assertThat(option10.displayName).isEqualTo("Foo")
+ assertThat(option10.screenReaderName).isEqualTo("Foo thing")
assertThat(option10.icon).isNull()
assertThat(option10.childSettings).isEmpty()
assertThat(option10.watchFaceEditorData).isNull()
val option11 = (setting1.options[1] as ListOption)
assertThat(option11.id).isEqualTo(UserStyleSetting.Option.Id("bar"))
assertThat(option11.displayName).isEqualTo("Bar")
+ assertThat(option11.screenReaderName).isEqualTo("Bar thing")
assertThat(option11.icon).isNull()
assertThat(option11.childSettings).isEmpty()
assertThat(option11.watchFaceEditorData).isNull()
@@ -117,11 +121,13 @@
val option20 = (setting2.options[0] as ListOption)
assertThat(option20.id).isEqualTo(UserStyleSetting.Option.Id("a"))
assertThat(option20.displayName).isEqualTo("A")
+ assertThat(option20.screenReaderName).isEqualTo("Option A")
assertThat(option20.icon).isNull()
assertThat(option20.childSettings).containsExactly(setting0)
val option21 = (setting2.options[1] as ListOption)
assertThat(option21.id).isEqualTo(UserStyleSetting.Option.Id("b"))
assertThat(option21.displayName).isEqualTo("B")
+ assertThat(option21.screenReaderName).isEqualTo("Option B")
assertThat(option21.icon).isNull()
assertThat(option21.childSettings).containsExactly(setting1)
parser.close()
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index 1c89c53..81c2872 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -21,6 +21,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.wear.watchface.style.test.R
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
import com.google.common.truth.Truth
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,6 +40,7 @@
setLocale(Locale.ENGLISH)
}
)
+
private val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
context.resources,
@@ -47,12 +52,14 @@
UserStyleSetting.Option.Id("red_style"),
context.resources,
R.string.red_style_name,
+ R.string.red_style_name,
null
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("green_style"),
context.resources,
R.string.green_style_name,
+ R.string.green_style_name,
null
)
),
@@ -106,4 +113,100 @@
).displayName
).isEqualTo("Stile verde")
}
+
+ @Test
+ public fun listOptionsWithIndices() {
+ val listUserStyleSetting = ListUserStyleSetting(
+ UserStyleSetting.Id("list"),
+ context.resources,
+ R.string.colors_style_setting,
+ R.string.colors_style_setting_description,
+ icon = null,
+ options = listOf(
+ ListOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null
+ ),
+ ListOption(
+ UserStyleSetting.Option.Id("two"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null
+ ),
+ ListOption(
+ UserStyleSetting.Option.Id("three"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null
+ )
+ ),
+ listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+ )
+
+ val option0 = listUserStyleSetting.options[0] as ListOption
+ Truth.assertThat(option0.displayName).isEqualTo("Option 1")
+ Truth.assertThat(option0.screenReaderName).isEqualTo("List option 1")
+
+ val option1 = listUserStyleSetting.options[1] as ListOption
+ Truth.assertThat(option1.displayName).isEqualTo("Option 2")
+ Truth.assertThat(option1.screenReaderName).isEqualTo("List option 2")
+
+ val option2 = listUserStyleSetting.options[2] as ListOption
+ Truth.assertThat(option2.displayName).isEqualTo("Option 3")
+ Truth.assertThat(option2.screenReaderName).isEqualTo("List option 3")
+ }
+
+ @Test
+ public fun complicationSlotsOptionsWithIndices() {
+ val complicationSetting = ComplicationSlotsUserStyleSetting(
+ UserStyleSetting.Id("complications_style_setting1"),
+ displayName = "Complications",
+ description = "Number and position",
+ icon = null,
+ complicationConfig = listOf(
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null,
+ emptyList()
+ ),
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("two"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null,
+ emptyList()
+ ),
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("three"),
+ context.resources,
+ R.string.ith_option,
+ R.string.ith_option_screen_reader_name,
+ icon = null,
+ emptyList()
+ )
+ ),
+ listOf(WatchFaceLayer.COMPLICATIONS)
+ )
+
+ val option0 = complicationSetting.options[0] as ComplicationSlotsOption
+ Truth.assertThat(option0.displayName).isEqualTo("Option 1")
+ Truth.assertThat(option0.screenReaderName).isEqualTo("List option 1")
+
+ val option1 = complicationSetting.options[1] as ComplicationSlotsOption
+ Truth.assertThat(option1.displayName).isEqualTo("Option 2")
+ Truth.assertThat(option1.screenReaderName).isEqualTo("List option 2")
+
+ val option2 = complicationSetting.options[2] as ComplicationSlotsOption
+ Truth.assertThat(option2.displayName).isEqualTo("Option 3")
+ Truth.assertThat(option2.screenReaderName).isEqualTo("List option 3")
+ }
}
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
index 3da6de0..4b8665e 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -17,7 +17,9 @@
<resources>
<string name="red_style_name">Red Style</string>
+ <string name="red_style_name_screen_reader">Red watch face style</string>
<string name="green_style_name">Green Style</string>
+ <string name="green_style_name_screen_reader">Green watch face style</string>
<string name="colors_style_setting">Colors</string>
<string name="colors_style_setting_description">Watchface colorization</string>
@@ -30,4 +32,7 @@
<string name="complication_name" translatable="false">Name</string>
<string name="complication_screen_reader_name" translatable="false">This is a name</string>
+
+ <string name="ith_option" translatable="false">Option %1$d</string>
+ <string name="ith_option_screen_reader_name" translatable="false">List option %1$d</string>
</resources>
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
index 7eed53d..0931672 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
@@ -28,6 +28,7 @@
<ListOption
android:icon="@drawable/red_icon"
app:displayName="@string/red_style_name"
+ app:nameForScreenReaders="@string/red_style_name_screen_reader"
app:id="red">
<OnWatchEditorData android:icon="@drawable/red_icon_wf"/>
</ListOption>
@@ -45,9 +46,11 @@
app:id="Thing2">
<ListOption
app:displayName="Foo"
+ app:nameForScreenReaders="Foo thing"
app:id="foo" />
<ListOption
app:displayName="Bar"
+ app:nameForScreenReaders="Bar thing"
app:id="bar" />
</ListUserStyleSetting>
<ListUserStyleSetting
@@ -57,11 +60,13 @@
app:id="TopLevel">
<ListOption
app:displayName="A"
+ app:nameForScreenReaders="Option A"
app:id="a">
<ChildSetting app:id="ColorStyle" />
</ListOption>
<ListOption
app:displayName="B"
+ app:nameForScreenReaders="Option B"
app:id="b">
<ChildSetting app:id="Thing2" />
</ListOption>
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
index d596af1e..81191e1 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
@@ -28,12 +28,14 @@
<ListOption
android:icon="@drawable/red_icon"
app:displayName="@string/red_style_name"
+ app:nameForScreenReaders="@string/red_style_name_screen_reader"
app:id="@string/list_setting_common_option_red_id">
<OnWatchEditorData android:icon="@drawable/red_icon_wf"/>
</ListOption>
<ListOption
android:icon="@drawable/green_icon"
app:displayName="@string/green_style_name"
+ app:nameForScreenReaders="@string/green_style_name_screen_reader"
app:id="@string/list_setting_common_option_green_id">
<OnWatchEditorData android:icon="@drawable/green_icon_wf"/>
</ListOption>
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 4691492..158391f 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -80,20 +80,26 @@
class CharSequenceDisplayText(private val charSequence: CharSequence) : DisplayText() {
override fun toCharSequence() = charSequence
+ // This is used purely to estimate the wireformat size.
override fun write(dos: DataOutputStream) {
- dos.writeUTF(charSequence.toString())
+ dos.writeUTF(toCharSequence().toString())
}
}
- class ResourceDisplayText(
- private val resources: Resources,
- @StringRes private val id: Int
+ open class ResourceDisplayText(
+ protected val resources: Resources,
+ @StringRes protected val id: Int
) : DisplayText() {
override fun toCharSequence() = resources.getString(id)
+ }
- override fun write(dos: DataOutputStream) {
- dos.writeInt(id)
- }
+ class ResourceDisplayTextWithIndex(
+ resources: Resources,
+ @StringRes id: Int,
+ ) : ResourceDisplayText(resources, id) {
+ var index: Int? = null
+
+ override fun toCharSequence() = resources.getString(id, index!!)
}
}
@@ -133,6 +139,30 @@
public val defaultOptionIndex: Int,
public val affectedWatchFaceLayers: Collection<WatchFaceLayer>
) {
+ init {
+ require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
+ "defaultOptionIndex must be within the range of the options list"
+ }
+
+ requireUniqueOptionIds(id, options)
+
+ // Assign 1 based indices to display names to allow names such as Option 1, Option 2,
+ // etc...
+ for ((index, option) in options.withIndex()) {
+ option.displayNameInternal?.let {
+ if (it is DisplayText.ResourceDisplayTextWithIndex) {
+ it.index = index + 1
+ }
+ }
+
+ option.screenReaderNameInternal?.let {
+ if (it is DisplayText.ResourceDisplayTextWithIndex) {
+ it.index = index + 1
+ }
+ }
+ }
+ }
+
/**
* Optional data for an on watch face editor (not the companion editor).
*
@@ -294,11 +324,16 @@
resources: Resources,
parser: XmlResourceParser,
attributeId: String,
- defaultValue: DisplayText? = null
+ defaultValue: DisplayText? = null,
+ indexedResourceNamesSupported: Boolean = false
): DisplayText {
val displayNameId = parser.getAttributeResourceValue(NAMESPACE_APP, attributeId, -1)
return if (displayNameId != -1) {
- DisplayText.ResourceDisplayText(resources, displayNameId)
+ if (indexedResourceNamesSupported) {
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameId)
+ } else {
+ DisplayText.ResourceDisplayText(resources, displayNameId)
+ }
} else if (parser.hasValue(attributeId) || defaultValue == null) {
DisplayText.CharSequenceDisplayText(
parser.getAttributeValue(NAMESPACE_APP, attributeId) ?: "")
@@ -430,12 +465,6 @@
}
}
- init {
- require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
- "defaultOptionIndex must be in the range [0 .. options.size)"
- }
- }
-
internal fun getSettingOptionForId(id: ByteArray?) =
if (id == null) {
options[defaultOptionIndex]
@@ -541,6 +570,12 @@
@SuppressWarnings("HiddenAbstractMethod")
internal abstract fun getUserStyleSettingClass(): Class<out UserStyleSetting>
+ internal open val displayNameInternal: DisplayText?
+ get() = null
+
+ internal open val screenReaderNameInternal: DisplayText?
+ get() = null
+
/**
* Machine readable identifier for [Option]s. The length of this identifier may not exceed
* [MAX_LENGTH].
@@ -887,6 +922,29 @@
* Not to be confused with complication data source selection.
*/
public class ComplicationSlotsUserStyleSetting : UserStyleSetting {
+ private constructor(
+ id: Id,
+ displayNameInternal: DisplayText,
+ descriptionInternal: DisplayText,
+ icon: Icon?,
+ watchFaceEditorData: WatchFaceEditorData?,
+ options: List<ComplicationSlotsOption>,
+ defaultOptionIndex: Int,
+ affectedWatchFaceLayers: Collection<WatchFaceLayer>
+ ) : super(
+ id,
+ displayNameInternal,
+ descriptionInternal,
+ icon,
+ watchFaceEditorData,
+ options,
+ defaultOptionIndex,
+ affectedWatchFaceLayers
+ ) {
+ require(affectedWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+ "ComplicationSlotsUserStyleSetting must affect the complications layer"
+ }
+ }
/**
* Overrides to be applied to the corresponding [androidx.wear.watchface.ComplicationSlot]'s
@@ -1212,8 +1270,8 @@
* [WatchFaceLayer.COMPLICATIONS].
* @param defaultOption The default option, used when data isn't persisted. Optional
* parameter which defaults to the first element of [complicationConfig].
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
- * to the companion and its contents may be used in preference to other fields by an on
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an on
* watch face editor.
* @hide
*/
@@ -1228,7 +1286,7 @@
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
defaultOption: ComplicationSlotsOption = complicationConfig.first(),
watchFaceEditorData: WatchFaceEditorData? = null
- ) : super(
+ ) : this(
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
@@ -1237,12 +1295,7 @@
complicationConfig,
complicationConfig.indexOf(defaultOption),
affectsWatchFaceLayers
- ) {
- require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
- "ComplicationSlotsUserStyleSetting must affect the complications layer"
- }
- requireUniqueOptionIds(id, complicationConfig)
- }
+ )
/**
* Constructs a ComplicationSlotsUserStyleSetting where
@@ -1264,8 +1317,8 @@
* [WatchFaceLayer.COMPLICATIONS].
* @param defaultOption The default option, used when data isn't persisted. Optional
* parameter which defaults to the first element of [complicationConfig].
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
- * to the companion and its contents may be used in preference to other fields by an on
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an on
* watch face editor.
*/
@JvmOverloads
@@ -1279,7 +1332,7 @@
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
defaultOption: ComplicationSlotsOption = complicationConfig.first(),
watchFaceEditorData: WatchFaceEditorData? = null
- ) : super(
+ ) : this(
id,
DisplayText.ResourceDisplayText(resources, displayNameResourceId),
DisplayText.ResourceDisplayText(resources, descriptionResourceId),
@@ -1288,12 +1341,7 @@
complicationConfig,
complicationConfig.indexOf(defaultOption),
affectsWatchFaceLayers
- ) {
- require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
- "ComplicationSlotsUserStyleSetting must affect the complications layer"
- }
- requireUniqueOptionIds(id, complicationConfig)
- }
+ )
internal constructor (
id: Id,
@@ -1304,7 +1352,7 @@
options: List<ComplicationSlotsOption>,
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
defaultOptionIndex: Int
- ) : super(
+ ) : this(
id,
displayName,
description,
@@ -1313,15 +1361,7 @@
options,
defaultOptionIndex,
affectsWatchFaceLayers
- ) {
- require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
- "defaultOptionIndex must be within the range of the options list"
- }
- require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
- "ComplicationSlotsUserStyleSetting must affect the complications layer"
- }
- requireUniqueOptionIds(id, options)
- }
+ )
internal constructor(
wireFormat: ComplicationsUserStyleSettingWireFormat
@@ -1335,6 +1375,16 @@
}
}
}
+ wireFormat.mPerOptionScreenReaderNames?.let { perOptionScreenReaderNames ->
+ val optionsIterator = options.iterator()
+ for (screenReaderName in perOptionScreenReaderNames) {
+ val option = optionsIterator.next() as ComplicationSlotsOption
+ screenReaderName?.let {
+ option.screenReaderNameInternal =
+ DisplayText.CharSequenceDisplayText(screenReaderName)
+ }
+ }
+ }
}
/** @hide */
@@ -1351,7 +1401,8 @@
watchFaceEditorData?.toWireFormat(),
options.map {
(it as ComplicationSlotsOption).watchFaceEditorData?.toWireFormat() ?: Bundle()
- }
+ },
+ options.map { (it as ComplicationSlotsOption).screenReaderName }
)
internal companion object {
@@ -1413,18 +1464,32 @@
*/
public class ComplicationSlotsOption : Option {
/**
- * Overlays to be applied when this ComplicationSlotsOption is selected. If this is empty
- * then the net result is the initial complication configuration.
+ * Overlays to be applied when this ComplicationSlotsOption is selected. If this is
+ * empty then the net result is the initial complication configuration.
*/
public val complicationSlotOverlays: Collection<ComplicationSlotOverlay>
/** Backing field for [displayName]. */
- private val displayNameInternal: DisplayText
+ override val displayNameInternal: DisplayText
- /** Localized human readable name for the setting, used in the style selection UI. */
+ /**
+ * Localized human readable name for the setting, used in the editor style selection UI.
+ * This should be short (ideally < 20 characters).
+ */
public val displayName: CharSequence
get() = displayNameInternal.toCharSequence()
+ /** Backing field for [screenReaderName]. */
+ override var screenReaderNameInternal: DisplayText?
+
+ /**
+ * Optional localized human readable name for the setting, used by screen readers. This
+ * should be more descriptive than [displayName]. Note prior to android T this is
+ * ignored by companion editors.
+ */
+ public val screenReaderName: CharSequence?
+ get() = screenReaderNameInternal?.toCharSequence()
+
/** Icon for use in the companion style selection UI. */
public val icon: Icon?
@@ -1441,15 +1506,17 @@
*
* @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the
- * userStyle selection UI.
+ * userStyle selection UI. This should be short, ideally < 20 characters.
+ * @param screenReaderName Localized human readable name for the element, used by
+ * screen readers. This should be more descriptive than [displayName].
* @param icon [Icon] for use in the companion style selection UI. This gets sent to the
* companion over bluetooth and should be small (ideally a few kb in size).
* @param complicationSlotOverlays Overlays to be applied when this
* ComplicationSlotsOption is selected. If this is empty then the net result is the
* initial complication configuration.
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
- * sent to the companion and its contents may be used in preference to other fields by
- * an on watch face editor.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -1457,12 +1524,15 @@
public constructor(
id: Id,
displayName: CharSequence,
+ screenReaderName: CharSequence,
icon: Icon?,
complicationSlotOverlays: Collection<ComplicationSlotOverlay>,
watchFaceEditorData: WatchFaceEditorData? = null
) : super(id, emptyList()) {
this.complicationSlotOverlays = complicationSlotOverlays
- this.displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+ displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+ screenReaderNameInternal =
+ DisplayText.CharSequenceDisplayText(screenReaderName)
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1474,16 +1544,20 @@
* @param id [Id] for the element, must be unique.
* @param resources The [Resources] from which [displayNameResourceId] is load.
* @param displayNameResourceId String resource id for a human readable name for the
- * element, used in the userStyle selection UI.
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$d` that will get replaced with
+ * the 1-based index of the ComplicationSlotsOption in the list of
+ * ComplicationSlotsOptions.
* @param icon [Icon] for use in the companion style selection UI. This gets sent to the
* companion over bluetooth and should be small (ideally a few kb in size).
* @param complicationSlotOverlays Overlays to be applied when this
* ComplicationSlotsOption is selected. If this is empty then the net result is the
* initial complication configuration.
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
- * to the companion and its contents may be used in preference to other fields by an on
- * watch face editor.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
*/
+ @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
@JvmOverloads
public constructor(
id: Id,
@@ -1494,8 +1568,54 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(id, emptyList()) {
this.complicationSlotOverlays = complicationSlotOverlays
+ displayNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ screenReaderNameInternal = null
+ this.icon = icon
+ this.watchFaceEditorData = watchFaceEditorData
+ }
+
+ /**
+ * Constructs a ComplicationSlotsUserStyleSetting with [displayName] constructed from
+ * Resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] is load.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$d` that will get replaced with
+ * the 1-based index of the ComplicationSlotsOption in the list of
+ * ComplicationSlotsOptions.
+ * @param screenReaderNameResourceId String resource id for a human readable name for
+ * the element, used by screen readers. This should be more descriptive than
+ * [displayNameResourceId]. Note if the resource string contains `%1$d` that will get
+ * replaced with the 1-based index of the ComplicationSlotsOption in the list of
+ * ComplicationSlotsOptions. Note prior to android T this is ignored by companion
+ * editors.
+ * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
+ * companion over bluetooth and should be small (ideally a few kb in size).
+ * @param complicationSlotOverlays Overlays to be applied when this
+ * ComplicationSlotsOption is selected. If this is empty then the net result is the
+ * initial complication configuration.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes screenReaderNameResourceId: Int,
+ icon: Icon?,
+ complicationSlotOverlays: Collection<ComplicationSlotOverlay>,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(id, emptyList()) {
+ this.complicationSlotOverlays = complicationSlotOverlays
this.displayNameInternal =
- DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ this.screenReaderNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1503,12 +1623,14 @@
internal constructor(
id: Id,
displayName: DisplayText,
+ screenReaderName: DisplayText,
icon: Icon?,
watchFaceEditorData: WatchFaceEditorData?,
complicationSlotOverlays: Collection<ComplicationSlotOverlay>
) : super(id, emptyList()) {
this.complicationSlotOverlays = complicationSlotOverlays
this.displayNameInternal = displayName
+ this.screenReaderNameInternal = screenReaderName
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1528,6 +1650,7 @@
)
}
displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
+ screenReaderNameInternal = null // This will get overwritten.
icon = wireFormat.mIcon
watchFaceEditorData = null // This will get overwritten.
}
@@ -1541,6 +1664,9 @@
@Px maxHeight: Int
): Int {
var sizeEstimate = id.value.size + displayName.length
+ screenReaderName?.let {
+ sizeEstimate + it.length
+ }
for (overlay in complicationSlotOverlays) {
sizeEstimate += overlay.estimateWireSizeInBytes()
}
@@ -1587,6 +1713,7 @@
overlay.write(dos)
}
displayNameInternal.write(dos)
+ screenReaderNameInternal?.write(dos)
icon?.write(dos)
watchFaceEditorData?.write(dos)
}
@@ -1604,7 +1731,15 @@
val displayName = createDisplayText(
resources,
parser,
- "displayName"
+ "displayName",
+ indexedResourceNamesSupported = true
+ )
+ val screenReaderName = createDisplayText(
+ resources,
+ parser,
+ "nameForScreenReaders",
+ defaultValue = displayName,
+ indexedResourceNamesSupported = true
)
val icon = createIcon(
resources,
@@ -1640,6 +1775,7 @@
return ComplicationSlotsOption(
Id(id),
displayName,
+ screenReaderName,
icon,
watchFaceEditorData,
complicationSlotOverlays
@@ -1963,9 +2099,7 @@
options,
options.indexOf(defaultOption),
affectsWatchFaceLayers
- ) {
- requireUniqueOptionIds(id, options)
- }
+ )
/**
* Constructs a ListUserStyleSetting where [ListUserStyleSetting.displayName] and
@@ -2008,9 +2142,7 @@
options,
options.indexOf(defaultOption),
affectsWatchFaceLayers
- ) {
- requireUniqueOptionIds(id, options)
- }
+ )
internal constructor (
id: Id,
@@ -2030,11 +2162,7 @@
options,
defaultOptionIndex,
affectsWatchFaceLayers
- ) {
- require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
- "defaultOptionIndex must be within the range of the options list"
- }
- }
+ )
internal constructor(wireFormat: ListUserStyleSettingWireFormat) : super(wireFormat) {
wireFormat.mPerOptionOnWatchFaceEditorBundles?.let { optionsOnWatchFaceEditorIcons ->
@@ -2046,6 +2174,16 @@
}
}
}
+ wireFormat.mPerOptionScreenReaderNames?.let { perOptionScreenReaderNames ->
+ val optionsIterator = options.iterator()
+ for (screenReaderName in perOptionScreenReaderNames) {
+ val option = optionsIterator.next() as ListOption
+ screenReaderName?.let {
+ option.screenReaderNameInternal =
+ DisplayText.CharSequenceDisplayText(screenReaderName)
+ }
+ }
+ }
}
/** @hide */
@@ -2060,7 +2198,8 @@
defaultOptionIndex,
affectedWatchFaceLayers.map { it.ordinal },
watchFaceEditorData?.toWireFormat(),
- options.map { (it as ListOption).watchFaceEditorData?.toWireFormat() ?: Bundle() }
+ options.map { (it as ListOption).watchFaceEditorData?.toWireFormat() ?: Bundle() },
+ options.map { (it as ListOption).screenReaderName }
)
internal companion object {
@@ -2117,12 +2256,26 @@
*/
public class ListOption : Option {
/** Backing field for [displayName]. */
- private val displayNameInternal: DisplayText
+ override val displayNameInternal: DisplayText
- /** Localized human readable name for the setting, used in the style selection UI. */
+ /**
+ * Localized human readable name for the setting, used in the editor style selection UI.
+ * This should be short (ideally < 20 characters).
+ */
public val displayName: CharSequence
get() = displayNameInternal.toCharSequence()
+ /** Backing field for [screenReaderName]. */
+ override var screenReaderNameInternal: DisplayText?
+
+ /**
+ * Optional localized human readable name for the setting, used by screen readers. This
+ * should be more descriptive than [displayName]. Note prior to android T this is
+ * ignored by companion editors.
+ */
+ public val screenReaderName: CharSequence?
+ get() = screenReaderNameInternal?.toCharSequence()
+
/** Icon for use in the companion style selection UI. */
public val icon: Icon?
@@ -2139,10 +2292,14 @@
*
* @param id The [Id] of this ListOption, must be unique within the
* [ListUserStyleSetting].
- * @param displayName Localized human readable name for the setting, used in the style
- * selection UI.
+ ** @param displayName Localized human readable name for the element, used in the
+ * userStyle selection UI. This should be short, ideally < 20 characters.
+ * @param screenReaderName Localized human readable name for the element, used by
+ * screen readers. This should be more descriptive than [displayName].
* @param icon [Icon] for use in the companion style selection UI. This gets sent to the
* companion over bluetooth and should be small (ideally a few kb in size).
+ * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
+ * child settings must be listed in [UserStyleSchema.userStyleSettings].
* @param watchFaceEditorData Optional data for an on watch face editor, this will not be
* sent to the companion and its contents may be used in preference to other fields by
* an on watch face editor.
@@ -2152,11 +2309,14 @@
constructor(
id: Id,
displayName: CharSequence,
+ screenReaderName: CharSequence,
icon: Icon?,
childSettings: Collection<UserStyleSetting> = emptyList(),
watchFaceEditorData: WatchFaceEditorData? = null
) : super(id, childSettings) {
displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+ screenReaderNameInternal =
+ DisplayText.CharSequenceDisplayText(screenReaderName)
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2168,14 +2328,17 @@
* [ListUserStyleSetting].
* @param resources The [Resources] used to load [displayNameResourceId].
* @param displayNameResourceId String resource id for a human readable name for the
- * setting, used in the style selection UI.
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$d` that will get replaced with
+ * the 1-based index of the ListOption in the list of ListOptions.
* @param icon [Icon] for use in the companion style selection UI. This gets sent to the
* companion over bluetooth and should be small (ideally a few kb in size)
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
- * sent to the companion and its contents may be used in preference to other fields by
- * an on watch face editor.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
*/
@JvmOverloads
+ @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
constructor(
id: Id,
resources: Resources,
@@ -2184,7 +2347,8 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(id, emptyList()) {
displayNameInternal =
- DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ screenReaderNameInternal = null
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2196,16 +2360,17 @@
* [ListUserStyleSetting].
* @param resources The [Resources] used to load [displayNameResourceId].
* @param displayNameResourceId String resource id for a human readable name for the
- * setting, used in the style selection UI.
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters.
* @param icon [Icon] for use in the style selection UI. This gets sent to the
* companion over bluetooth and should be small (ideally a few kb in size).
- * These must be in
* @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
* child settings must be listed in [UserStyleSchema.userStyleSettings].
- * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
- * sent to the companion and its contents may be used in preference to other fields by
- * an on watch face editor.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
*/
+ @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
constructor(
id: Id,
resources: Resources,
@@ -2215,7 +2380,49 @@
watchFaceEditorData: WatchFaceEditorData? = null
) : super(id, childSettings) {
displayNameInternal =
- DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ screenReaderNameInternal = null
+ this.icon = icon
+ this.watchFaceEditorData = watchFaceEditorData
+ }
+
+ /**
+ * Constructs a ListOption.
+ *
+ * @param id The [Id] of this ListOption, must be unique within the
+ * [ListUserStyleSetting].
+ * @param resources The [Resources] used to load [displayNameResourceId].
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$d` that will get replaced with
+ * the 1-based index of the ListOption in the list of ListOptions.
+ * @param screenReaderNameResourceId String resource id for a human readable name for
+ * the element, used by screen readers. This should be more descriptive than
+ * [displayNameResourceId]. Note if the resource string contains `%1$d` that will get
+ * replaced with the 1-based index of the ListOption in the list of ListOptions. Note
+ * prior to android T this is ignored by companion editors.
+ * @param icon [Icon] for use in the style selection UI. This gets sent to the
+ * companion over bluetooth and should be small (ideally a few kb in size).
+ * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
+ * child settings must be listed in [UserStyleSchema.userStyleSettings].
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
+ */
+ @JvmOverloads
+ constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes screenReaderNameResourceId: Int,
+ icon: Icon?,
+ childSettings: Collection<UserStyleSetting> = emptyList(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(id, childSettings) {
+ displayNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ screenReaderNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2223,11 +2430,13 @@
internal constructor(
id: Id,
displayName: DisplayText,
+ screenReaderName: DisplayText,
icon: Icon?,
watchFaceEditorData: WatchFaceEditorData?,
childSettings: Collection<UserStyleSetting> = emptyList()
) : super(id, childSettings) {
displayNameInternal = displayName
+ screenReaderNameInternal = screenReaderName
this.icon = icon
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2236,6 +2445,7 @@
wireFormat: ListOptionWireFormat
) : super(Id(wireFormat.mId), ArrayList()) {
displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
+ screenReaderNameInternal = null // This will get overwritten.
icon = wireFormat.mIcon
watchFaceEditorData = null // This gets overwritten.
}
@@ -2249,6 +2459,9 @@
@Px maxHeight: Int
): Int {
var sizeEstimate = id.value.size + displayName.length
+ screenReaderName?.let {
+ sizeEstimate + it.length
+ }
icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
wireSizeAndDimensions.wireSizeBytes?.let {
sizeEstimate += it
@@ -2277,6 +2490,7 @@
override fun write(dos: DataOutputStream) {
dos.write(id.value)
displayNameInternal.write(dos)
+ screenReaderNameInternal?.write(dos)
icon?.write(dos)
watchFaceEditorData?.write(dos)
}
@@ -2293,7 +2507,15 @@
val displayName = createDisplayText(
resources,
parser,
- "displayName"
+ "displayName",
+ indexedResourceNamesSupported = true
+ )
+ val screenReaderName = createDisplayText(
+ resources,
+ parser,
+ "nameForScreenReaders",
+ defaultValue = displayName,
+ indexedResourceNamesSupported = true
)
val icon = createIcon(
resources,
@@ -2333,6 +2555,7 @@
return ListOption(
Id(id),
displayName,
+ screenReaderName,
icon,
watchFaceEditorData,
childSettings
diff --git a/wear/watchface/watchface-style/src/main/res/values/attrs.xml b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
index 996af90..915948b 100644
--- a/wear/watchface/watchface-style/src/main/res/values/attrs.xml
+++ b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
@@ -14,9 +14,10 @@
limitations under the License.
-->
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
<attr name="defaultOptionIndex" format="integer" />
<attr name="displayName" format="string" />
+ <attr name="nameForScreenReaders" format="string|reference" tools:ignore="MissingDefaultResource" />
<attr name="id" format="reference|string" />
<attr name="description" format="string" />
<attr name="accessibilityTraversalIndex" format="integer" />
@@ -178,6 +179,9 @@
<attr name="id" />
<!-- A displayName is required. Consider referencing a string resource for localization. -->
<attr name="displayName" />
+ <!-- A nameForScreenReaders is optional. Consider referencing a string resource for
+ localization. -->
+ <attr name="nameForScreenReaders" />
<!-- A link to a companion editor icon is optional but encouraged. -->
<attr name="android:icon" />
</declare-styleable>
@@ -189,6 +193,9 @@
<attr name="id" />
<!-- A displayName is required. Consider referencing a string resource for localization. -->
<attr name="displayName" />
+ <!-- A nameForScreenReaders is optional. Consider referencing a string resource for
+ localization. -->
+ <attr name="nameForScreenReaders" />
<!-- A link to a companion editor icon is optional but encouraged. -->
<attr name="android:icon" />
</declare-styleable>
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 8afe47f..cdd035f 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -37,14 +37,26 @@
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
-private val redStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
+private val redStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("red_style"),
+ "Red",
+ "Red",
+ icon = null
+)
-private val greenStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
+private val greenStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("green_style"),
+ "Green",
+ "Green",
+ icon = null
+)
-private val blueStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
+private val blueStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("blue_style"),
+ "Blue",
+ "Blue",
+ icon = null
+)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
@@ -57,14 +69,26 @@
listOf(WatchFaceLayer.BASE)
)
-private val classicStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
+private val classicStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("classic_style"),
+ "Classic",
+ "Classic",
+ icon = null
+)
-private val modernStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
+private val modernStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("modern_style"),
+ "Modern",
+ "Modern",
+ icon = null
+)
-private val gothicStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+private val gothicStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("gothic_style"),
+ "Gothic",
+ "Gothic",
+ icon = null
+)
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
@@ -352,21 +376,25 @@
val option0 = ListUserStyleSetting.ListOption(
Option.Id("0"),
"option 0",
+ "option 0",
icon = null
)
val option1 = ListUserStyleSetting.ListOption(
Option.Id("1"),
"option 1",
+ "option 1",
icon = null
)
val option0Copy = ListUserStyleSetting.ListOption(
Option.Id("0"),
"option #0",
+ "option #0",
icon = null
)
val option1Copy = ListUserStyleSetting.ListOption(
Option.Id("1"),
"option #1",
+ "option #1",
icon = null
)
val setting = ListUserStyleSetting(
@@ -686,10 +714,10 @@
@Test
fun hierarchicalStyle() {
val twelveHourClockOption =
- ListUserStyleSetting.ListOption(Option.Id("12_style"), "12", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("12_style"), "12", "12", icon = null)
val twentyFourHourClockOption =
- ListUserStyleSetting.ListOption(Option.Id("24_style"), "24", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("24_style"), "24", "24", icon = null)
val digitalClockStyleSetting = ListUserStyleSetting(
UserStyleSetting.Id("digital_clock_style"),
@@ -703,6 +731,7 @@
val digitalWatchFaceType = ListUserStyleSetting.ListOption(
Option.Id("digital"),
"Digital",
+ "Digital",
icon = null,
childSettings = listOf(digitalClockStyleSetting, colorStyleSetting)
)
@@ -710,6 +739,7 @@
val analogWatchFaceType = ListUserStyleSetting.ListOption(
Option.Id("analog"),
"Analog",
+ "Analog",
icon = null,
childSettings = listOf(watchHandLengthStyleSetting, watchHandStyleSetting)
)
@@ -747,6 +777,7 @@
val leftAndRightComplications = ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
displayName = "Both",
+ screenReaderName = "Both complications",
icon = null,
emptyList()
)
@@ -769,12 +800,14 @@
val optionA1 = ListUserStyleSetting.ListOption(
Option.Id("a1_style"),
displayName = "A1",
+ screenReaderName = "A1 style",
icon = null,
childSettings = listOf(complicationSetting1, complicationSetting2)
)
val optionA2 = ListUserStyleSetting.ListOption(
Option.Id("a2_style"),
displayName = "A2",
+ screenReaderName = "A2 style",
icon = null,
childSettings = listOf(complicationSetting2)
)
@@ -802,6 +835,7 @@
val leftAndRightComplications = ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
displayName = "Both",
+ screenReaderName = "Both complications",
icon = null,
emptyList()
)
@@ -824,12 +858,14 @@
val optionA1 = ListUserStyleSetting.ListOption(
Option.Id("a1_style"),
displayName = "A1",
+ screenReaderName = "A1 style",
icon = null,
childSettings = listOf(complicationSetting1)
)
val optionA2 = ListUserStyleSetting.ListOption(
Option.Id("a2_style"),
displayName = "A2",
+ screenReaderName = "A2 style",
icon = null
)
@@ -871,12 +907,14 @@
val leftAndRightComplications = ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
displayName = "Both",
+ screenReaderName = "Both complications",
icon = null,
emptyList()
)
val noComplications = ComplicationSlotsOption(
Option.Id("NO_COMPLICATIONS"),
displayName = "None",
+ screenReaderName = "No complications",
icon = null,
listOf(
ComplicationSlotOverlay(leftComplicationID, enabled = false),
@@ -895,12 +933,14 @@
val leftComplication = ComplicationSlotsOption(
Option.Id("LEFT_COMPLICATION"),
displayName = "Left",
+ screenReaderName = "Left complication",
icon = null,
listOf(ComplicationSlotOverlay(rightComplicationID, enabled = false))
)
val rightComplication = ComplicationSlotsOption(
Option.Id("RIGHT_COMPLICATION"),
displayName = "Right",
+ screenReaderName = "Right complication",
icon = null,
listOf(ComplicationSlotOverlay(leftComplicationID, enabled = false))
)
@@ -916,12 +956,14 @@
val normal = ComplicationSlotsOption(
Option.Id("Normal"),
displayName = "Normal",
+ screenReaderName = "Normal",
icon = null,
emptyList()
)
val traversal = ComplicationSlotsOption(
Option.Id("Traversal"),
displayName = "Traversal",
+ screenReaderName = "Traversal",
icon = null,
listOf(
ComplicationSlotOverlay(leftComplicationID, accessibilityTraversalIndex = 3),
@@ -940,17 +982,23 @@
val optionA1 = ListUserStyleSetting.ListOption(
Option.Id("a1_style"),
displayName = "A1",
+ screenReaderName = "A1 style",
icon = null,
childSettings = listOf(complicationSetting1)
)
val optionA2 = ListUserStyleSetting.ListOption(
Option.Id("a2_style"),
displayName = "A2",
+ screenReaderName = "A2 style",
icon = null,
childSettings = listOf(complicationSetting2)
)
- val optionA3 =
- ListUserStyleSetting.ListOption(Option.Id("a3_style"), "A3", icon = null)
+ val optionA3 = ListUserStyleSetting.ListOption(
+ Option.Id("a3_style"),
+ "A3",
+ screenReaderName = "A3 style",
+ icon = null
+ )
val a123Choice = ListUserStyleSetting(
UserStyleSetting.Id("a123"),
@@ -964,11 +1012,16 @@
val optionB1 = ListUserStyleSetting.ListOption(
Option.Id("b1_style"),
displayName = "B1",
+ screenReaderName = "B1 style",
icon = null,
childSettings = listOf(complicationSetting3)
)
- val optionB2 =
- ListUserStyleSetting.ListOption(Option.Id("b2_style"), "B2", icon = null)
+ val optionB2 = ListUserStyleSetting.ListOption(
+ Option.Id("b2_style"),
+ "B2",
+ screenReaderName = "B2 style",
+ icon = null
+ )
val b12Choice = ListUserStyleSetting(
UserStyleSetting.Id("b12"),
@@ -982,12 +1035,14 @@
val rootOptionA = ListUserStyleSetting.ListOption(
Option.Id("a_style"),
displayName = "A",
+ screenReaderName = "A style",
icon = null,
childSettings = listOf(a123Choice)
)
val rootOptionB = ListUserStyleSetting.ListOption(
Option.Id("b_style"),
displayName = "B",
+ screenReaderName = "B style",
icon = null,
childSettings = listOf(b12Choice)
)
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index cd39c27..4157013 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -60,24 +60,28 @@
private val option1 = ListOption(
Option.Id("1"),
"one",
+ "one screen reader",
icon1,
watchFaceEditorData = WatchFaceEditorData(wfIcon1)
)
private val option2 = ListOption(
Option.Id("2"),
"two",
+ "two screen reader",
icon2,
watchFaceEditorData = WatchFaceEditorData(wfIcon2)
)
private val option3 = ListOption(
Option.Id("3"),
"three",
+ "three screen reader",
icon3,
watchFaceEditorData = WatchFaceEditorData(wfIcon3)
)
private val option4 = ListOption(
Option.Id("4"),
"four",
+ "four screen reader",
icon4,
watchFaceEditorData = WatchFaceEditorData(wfIcon4)
)
@@ -277,10 +281,10 @@
@Suppress("Deprecation") // userStyleSettings
public fun parcelAndUnparcelHierarchicalSchema() {
val twelveHourClockOption =
- ListOption(Option.Id("12_style"), "12", icon = null)
+ ListOption(Option.Id("12_style"), "12", "12", icon = null)
val twentyFourHourClockOption =
- ListOption(Option.Id("24_style"), "24", icon = null)
+ ListOption(Option.Id("24_style"), "24", "24", icon = null)
val digitalClockStyleSetting = ListUserStyleSetting(
UserStyleSetting.Id("digital_clock_style"),
@@ -294,6 +298,7 @@
val digitalWatchFaceType = ListOption(
Option.Id("digital"),
"Digital",
+ "Digital setting",
icon = null,
childSettings = listOf(digitalClockStyleSetting)
)
@@ -322,6 +327,7 @@
val analogWatchFaceType = ListOption(
Option.Id("analog"),
"Analog",
+ "Analog setting",
icon = null,
childSettings = listOf(styleSetting1, styleSetting2)
)
@@ -556,12 +562,14 @@
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
"Both",
+ "Both complications visible",
null,
listOf()
),
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("NO_COMPLICATIONS"),
"None",
+ "No complications visible",
null,
listOf(
ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -577,6 +585,7 @@
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_COMPLICATION"),
"Left",
+ "Left complication visible",
null,
listOf(
ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -594,6 +603,7 @@
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("RIGHT_COMPLICATION"),
"Right",
+ "Right complication visible",
null,
listOf(
ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -605,6 +615,7 @@
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("RIGHT_COMPLICATION_MOVED"),
"MoveRight",
+ "Right complication moved",
null,
listOf(
ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
index 5528d83..0101694 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
@@ -269,25 +269,29 @@
icon = null,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
- UserStyleSetting.Option.Id("both"),
+ Option.Id("both"),
+ "left and right complications",
"left and right complications",
icon = null,
listOf(leftComplicationSlot, rightComplicationSlot),
),
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
- UserStyleSetting.Option.Id("left"),
+ Option.Id("left"),
+ "left complication",
"left complication",
icon = null,
listOf(leftComplicationSlot),
),
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
- UserStyleSetting.Option.Id("right"),
+ Option.Id("right"),
+ "right complication",
"right complication",
icon = null,
listOf(rightComplicationSlot),
),
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
- UserStyleSetting.Option.Id("both"),
+ Option.Id("both"),
+ "right and left complications",
"right and left complications",
icon = null,
listOf(rightComplicationSlot, leftComplicationSlot),
@@ -310,21 +314,25 @@
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("plain"),
"plain hands",
+ "plain hands",
icon = null
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("florescent"),
"florescent hands",
+ "florescent hands",
icon = null
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("thick"),
"thick hands",
+ "thick hands",
icon = null
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("plain"),
"simple hands",
+ "simple hands",
icon = null
)
),
@@ -392,6 +400,7 @@
val option = UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("both"),
"right and left complications",
+ "right and left complications",
icon = null,
listOf(rightComplicationSlot, leftComplicationSlot),
)
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index d2186c6..c70d866 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -90,18 +90,21 @@
Option.Id(RED_STYLE),
resources,
R.string.colors_style_red,
+ R.string.colors_style_red_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
),
ListUserStyleSetting.ListOption(
Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
+ R.string.colors_style_green_screen_reader,
Icon.createWithResource(this, R.drawable.green_style)
),
ListUserStyleSetting.ListOption(
Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
+ R.string.colors_style_blue_screen_reader,
Icon.createWithResource(this, R.drawable.blue_style)
)
),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index 3fd4f52..5f497f3 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -87,18 +87,21 @@
Option.Id(RED_STYLE),
resources,
R.string.colors_style_red,
+ R.string.colors_style_red_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
+ R.string.colors_style_green_screen_reader,
Icon.createWithResource(this, R.drawable.green_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
+ R.string.colors_style_blue_screen_reader,
Icon.createWithResource(this, R.drawable.blue_style)
)
),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 1ae8d16..766c9e5 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -62,6 +62,7 @@
UserStyleSetting.Option.Id("12_style"),
resources,
R.string.digital_clock_style_12,
+ R.string.digital_clock_style_12_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
)
}
@@ -71,6 +72,7 @@
UserStyleSetting.Option.Id("24_style"),
resources,
R.string.digital_clock_style_24,
+ R.string.digital_clock_style_24_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
)
}
@@ -133,6 +135,7 @@
UserStyleSetting.Option.Id(RED_STYLE),
resources,
R.string.colors_style_red,
+ R.string.colors_style_red_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
)
}
@@ -142,6 +145,7 @@
UserStyleSetting.Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
+ R.string.colors_style_green_screen_reader,
Icon.createWithResource(this, R.drawable.green_style)
)
}
@@ -151,6 +155,7 @@
UserStyleSetting.Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
+ R.string.colors_style_blue_screen_reader,
Icon.createWithResource(this, R.drawable.blue_style)
)
}
@@ -236,6 +241,7 @@
UserStyleSetting.Option.Id("digital"),
resources,
R.string.style_digital_watch,
+ R.string.style_digital_watch_screen_reader,
icon = Icon.createWithResource(this, R.drawable.d),
childSettings = listOf(
digitalClockStyleSetting,
@@ -250,6 +256,7 @@
UserStyleSetting.Option.Id("analog"),
resources,
R.string.style_analog_watch,
+ R.string.style_analog_watch_screen_reader,
icon = Icon.createWithResource(this, R.drawable.a),
childSettings = listOf(
colorStyleSetting,
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index 7e40c2a..cb5ad23 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -56,12 +56,14 @@
UserStyleSetting.Option.Id("yellow_style"),
resources,
R.string.colors_style_yellow,
+ R.string.colors_style_yellow_screen_reader,
Icon.createWithResource(this, R.drawable.yellow_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("blue_style"),
resources,
R.string.colors_style_blue,
+ R.string.colors_style_blue_screen_reader,
Icon.createWithResource(this, R.drawable.blue_style)
)
),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 72ec126..5d59314 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -81,12 +81,14 @@
Option.Id("red_style"),
resources,
R.string.colors_style_red,
+ R.string.colors_style_red_screen_reader,
Icon.createWithResource(this, R.drawable.red_style)
),
ListUserStyleSetting.ListOption(
Option.Id("green_style"),
resources,
R.string.colors_style_green,
+ R.string.colors_style_green_screen_reader,
Icon.createWithResource(this, R.drawable.green_style)
)
),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index c2404ff..622c224 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -76,18 +76,21 @@
Option.Id(RED_STYLE),
resources,
R.string.colors_style_red,
+ R.string.colors_style_red_screen_reader,
icon = null
),
ListUserStyleSetting.ListOption(
Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
+ R.string.colors_style_green_screen_reader,
icon = null
),
ListUserStyleSetting.ListOption(
Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
+ R.string.colors_style_blue_screen_reader,
icon = null
)
),
@@ -108,18 +111,21 @@
Option.Id(CLASSIC_STYLE),
resources,
R.string.hand_style_classic,
+ R.string.hand_style_classic_screen_reader,
icon = null
),
ListUserStyleSetting.ListOption(
Option.Id(MODERN_STYLE),
resources,
R.string.hand_style_modern,
+ R.string.hand_style_modern_screen_reader,
icon = null
),
ListUserStyleSetting.ListOption(
Option.Id(GOTHIC_STYLE),
resources,
R.string.hand_style_gothic,
+ R.string.hand_style_gothic_screen_reader,
icon = null
)
),
diff --git a/wear/watchface/watchface/samples/src/main/res/values/strings.xml b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
index de9835e..8dce3e9 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -54,6 +54,18 @@
<!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
<string name="colors_style_yellow">Yellow</string>
+ <!-- An option within the watch face color style theme settings -->
+ <string name="colors_style_red_screen_reader">Red color theme</string>
+
+ <!-- An option within the watch face color style theme settings -->
+ <string name="colors_style_green_screen_reader">Green color theme</string>
+
+ <!-- An option within the watch face color style theme settings -->
+ <string name="colors_style_blue_screen_reader">Blue color theme</string>
+
+ <!-- An option within the watch face color style theme settings -->
+ <string name="colors_style_yellow_screen_reader">Yellow color theme</string>
+
<!-- Name of watchface style category for selecting the hand style [CHAR LIMIT=20] -->
<string name="hand_style_setting" translatable="false">Hand Style</string>
@@ -63,12 +75,24 @@
<!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
<string name="hand_style_classic" translatable="false">Classic</string>
+ <!-- An option with the watch face hand style settings for use by screen readers -->
+ <string name="hand_style_classic_screen_reader" translatable="false">Classic watch hand
+ style</string>
+
<!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
<string name="hand_style_modern" translatable="false">Modern</string>
+ <!-- An option with the watch face hand style settings for use by screen readers -->
+ <string name="hand_style_modern_screen_reader" translatable="false">Modern watch hand
+ style</string>
+
<!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
<string name="hand_style_gothic" translatable="false">Gothic</string>
+ <!-- An option with the watch face hand style settings for use by screen readers -->
+ <string name="hand_style_gothic_screen_reader" translatable="false">Gothic watch hand
+ style</string>
+
<!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
<string name="watchface_pips_setting">Hour Pips</string>
@@ -171,15 +195,27 @@
<!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
<string name="digital_clock_style_12">12</string>
- <!-- Menu option for a 24 hour digital clock display. [CHAR LIMIT=20] -->
+ <!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
+ <string name="digital_clock_style_12_screen_reader">12 hour clock</string>
+
+ <!-- Menu option for a 24 hour digital clock display used by screen reader. -->
<string name="digital_clock_style_24">24</string>
+ <!-- Menu option for a 24 hour digital clock display used by screen reader.-->
+ <string name="digital_clock_style_24_screen_reader">24 hour clock</string>
+
<!-- Menu option for selecting a digital clock [CHAR LIMIT=20] -->
<string name="style_digital_watch">Digital</string>
+ <!-- Menu option for selecting a digital clock from a screen reader. -->
+ <string name="style_digital_watch_screen_reader">Digital watch style</string>
+
<!-- Menu option for selecting an analog clock [CHAR LIMIT=20] -->
<string name="style_analog_watch">Analog</string>
+ <!-- Menu option for selecting an analog clock from a screen reader.-->
+ <string name="style_analog_watch_screen_reader">Analog watch style </string>
+
<!-- Title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
<string name="clock_type">Clock type</string>
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
index 2f2f817..7f3f0ad 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
@@ -38,28 +38,31 @@
val engine = serviceSpy.onCreateEngine() as WatchFaceService.EngineWrapper
try {
- val schema = UserStyleSchema(listOf(
- UserStyleSetting.ListUserStyleSetting(
- UserStyleSetting.Id("someId"),
- "displayName",
- "description",
- Icon.createWithResource(
- context,
- androidx.wear.watchface.test.R.drawable.example_icon_24
- ),
- listOf(
- UserStyleSetting.ListUserStyleSetting.ListOption(
- UserStyleSetting.Option.Id("red_style"),
- displayName = "Red",
- icon = Icon.createWithResource(
- context,
- androidx.wear.watchface.test.R.drawable.example_icon_24
- ),
- )
- ),
- listOf(WatchFaceLayer.BASE)
+ val schema = UserStyleSchema(
+ listOf(
+ UserStyleSetting.ListUserStyleSetting(
+ UserStyleSetting.Id("someId"),
+ "displayName",
+ "description",
+ Icon.createWithResource(
+ context,
+ androidx.wear.watchface.test.R.drawable.example_icon_24
+ ),
+ listOf(
+ UserStyleSetting.ListUserStyleSetting.ListOption(
+ UserStyleSetting.Option.Id("red_style"),
+ displayName = "Red",
+ screenReaderName = "Red watchface style",
+ icon = Icon.createWithResource(
+ context,
+ androidx.wear.watchface.test.R.drawable.example_icon_24
+ ),
+ )
+ ),
+ listOf(WatchFaceLayer.BASE)
+ )
)
- ))
+ )
// expect no exception
engine.validateSchemaWireSize(schema)
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 520e72f..7bcd27e 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -195,13 +195,13 @@
private val complicationDrawableBackground = ComplicationDrawable(context)
private val redStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
private val greenStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
private val blueStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
@@ -214,14 +214,26 @@
listOf(WatchFaceLayer.BASE)
)
- private val classicStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
+ private val classicStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("classic_style"),
+ "Classic",
+ "Classic",
+ icon = null
+ )
- private val modernStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
+ private val modernStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("modern_style"),
+ "Modern",
+ "Modern",
+ icon = null
+ )
- private val gothicStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+ private val gothicStyleOption = ListUserStyleSetting.ListOption(
+ Option.Id("gothic_style"),
+ "Gothic",
+ "Gothic",
+ icon = null
+ )
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
@@ -236,7 +248,7 @@
)
private val badStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", "Bad", icon = null)
@Suppress("DEPRECATION") // setDefaultDataSourceType
private val leftComplication =
@@ -356,6 +368,7 @@
private val leftAndRightComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left and Right",
+ "Left and Right complications",
null,
// An empty list means use the initial config.
emptyList()
@@ -363,6 +376,7 @@
private val noComplicationsOption = ComplicationSlotsOption(
Option.Id(NO_COMPLICATIONS),
"None",
+ "No complications",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -374,6 +388,7 @@
private val leftOnlyComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_COMPLICATION),
"Left",
+ "Left complication",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -385,6 +400,7 @@
private val rightOnlyComplicationsOption = ComplicationSlotsOption(
Option.Id(RIGHT_COMPLICATION),
"Right",
+ "Right complication",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -2027,6 +2043,7 @@
val rightAndSelectComplicationsOption = ComplicationSlotsOption(
Option.Id(RIGHT_AND_LEFT_COMPLICATIONS),
"Right and Left",
+ "Right and Left complications",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -2648,6 +2665,7 @@
ComplicationSlotsOption(
Option.Id("one"),
"one",
+ "one",
null,
listOf(
ComplicationSlotOverlay(
@@ -2658,7 +2676,8 @@
),
ComplicationSlotsOption(
Option.Id("two"),
- "teo",
+ "two",
+ "two",
null,
listOf(
ComplicationSlotOverlay(
@@ -2720,16 +2739,23 @@
val option1 = ListUserStyleSetting.ListOption(
Option.Id("1"),
displayName = "1",
+ screenReaderName = "1",
icon = null,
childSettings = listOf(complicationsStyleSetting)
)
val option2 = ListUserStyleSetting.ListOption(
Option.Id("2"),
displayName = "2",
+ screenReaderName = "2",
icon = null,
childSettings = listOf(complicationsStyleSetting2)
)
- val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "3", icon = null)
+ val option3 = ListUserStyleSetting.ListOption(
+ Option.Id("3"),
+ displayName = "3",
+ screenReaderName = "3",
+ icon = null
+ )
val choice = ListUserStyleSetting(
UserStyleSetting.Id("123"),
displayName = "123",
@@ -3919,6 +3945,7 @@
val rightComplicationBoundsOption = ComplicationSlotsOption(
Option.Id(RIGHT_COMPLICATION),
"Right",
+ "Right",
null,
listOf(
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
@@ -3936,6 +3963,7 @@
ComplicationSlotsOption(
Option.Id("Default"),
"Default",
+ "Default",
null,
emptyList()
),
@@ -4347,7 +4375,7 @@
val longOptionsList = ArrayList<ListUserStyleSetting.ListOption>()
for (i in 0..10000) {
longOptionsList.add(
- ListUserStyleSetting.ListOption(Option.Id("id$i"), "Name", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("id$i"), "Name", "Name", icon = null)
)
}
val tooLargeList = ListUserStyleSetting(
@@ -5413,6 +5441,7 @@
ComplicationSlotsOption(
Option.Id("123"),
"testOption",
+ "testOption",
icon = null,
complicationSlotOverlays = listOf(
ComplicationSlotOverlay(
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index 91ae917..acad3fa 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -43,9 +43,10 @@
WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
return;
}
- ProcessGlobalConfig.createInstance()
- .setDataDirectorySuffix(this, "per_process_webview_data_0")
- .apply();
+ ProcessGlobalConfig config = new ProcessGlobalConfig();
+ config.setDataDirectorySuffix(this,
+ "per_process_webview_data_0");
+ ProcessGlobalConfig.apply(config);
setContentView(R.layout.activity_process_global_config);
WebView wv = findViewById(R.id.process_global_config_webview);
wv.setWebViewClient(new WebViewClient());
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -10,8 +10,8 @@
}
public class ProcessGlobalConfig {
- method public void apply();
- method public static androidx.webkit.ProcessGlobalConfig createInstance();
+ ctor public ProcessGlobalConfig();
+ method public static void apply(androidx.webkit.ProcessGlobalConfig);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
}
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -10,8 +10,8 @@
}
public class ProcessGlobalConfig {
- method public void apply();
- method public static androidx.webkit.ProcessGlobalConfig createInstance();
+ ctor public ProcessGlobalConfig();
+ method public static void apply(androidx.webkit.ProcessGlobalConfig);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
}
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -10,8 +10,8 @@
}
public class ProcessGlobalConfig {
- method public void apply();
- method public static androidx.webkit.ProcessGlobalConfig createInstance();
+ ctor public ProcessGlobalConfig();
+ method public static void apply(androidx.webkit.ProcessGlobalConfig);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
index bdcfa0f..b193b2a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
@@ -18,8 +18,10 @@
import android.content.Context;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.ApiHelperForP;
import androidx.webkit.internal.StartupApiFeature;
import androidx.webkit.internal.WebViewFeatureInternal;
@@ -28,7 +30,6 @@
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -37,7 +38,8 @@
* WebView has some process-global configuration parameters that cannot be changed once WebView has
* been loaded. This class allows apps to set these parameters.
* <p>
- * If it is used, the configuration should be set and {@link #apply()} should be called prior to
+ * If it is used, the configuration should be set and
+ * {@link #apply(androidx.webkit.ProcessGlobalConfig)} should be called prior to
* loading WebView into the calling process. Most of the methods in
* {@link android.webkit} and {@link androidx.webkit} packages load WebView, so the
* configuration should be applied before calling any of these methods.
@@ -45,13 +47,12 @@
* The following code configures the data directory suffix that WebView
* uses and then applies the configuration. WebView uses this configuration when it is loaded.
* <pre class="prettyprint">
- * ProcessGlobalConfig.createInstance()
- * .setDataDirectorySuffix("random_suffix")
- * .apply();
+ * ProcessGlobalConfig config = new ProcessGlobalConfig();
+ * config.setDataDirectorySuffix("random_suffix")
+ * ProcessGlobalConfig.apply(config);
* </pre>
* <p>
- * Restrictions are in place to ensure that {@link #createInstance()} can only be called once.
- * The setters and {@link #apply()} can also only be called once.
+ * {@link ProcessGlobalConfig#apply(androidx.webkit.ProcessGlobalConfig)} can only be called once.
* <p>
* Only a single thread should access this class at a given time.
* <p>
@@ -61,87 +62,16 @@
public class ProcessGlobalConfig {
private static final AtomicReference<HashMap<String, Object>> sProcessGlobalConfig =
new AtomicReference<HashMap<String, Object>>();
- private static AtomicBoolean sInstanceCreated = new AtomicBoolean(false);
- private boolean mApplyCalled = false;
- private String mDataDirectorySuffix;
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static boolean sApplyCalled = false;
+ String mDataDirectorySuffix;
- private ProcessGlobalConfig() {
- }
/**
- * Creates instance of {@link ProcessGlobalConfig}.
- *
- * This method can only be called once.
- *
- * @return {@link ProcessGlobalConfig} object where configuration can be set and applied
- *
- * @throws IllegalStateException if this method was called before
+ * Creates a {@link ProcessGlobalConfig} object.
*/
- @NonNull
- public static ProcessGlobalConfig createInstance() {
- if (!sInstanceCreated.compareAndSet(false, true)) {
- throw new IllegalStateException("ProcessGlobalConfig#createInstance was "
- + "called more than once, which is an illegal operation. The configuration "
- + "settings provided by ProcessGlobalConfig take effect only once, when "
- + "WebView is first loaded into the current process. Every process should "
- + "only ever create a single instance of ProcessGlobalConfig and apply it "
- + "once, before any calls to android.webkit APIs, such as during early app "
- + "startup."
- );
- }
- return new ProcessGlobalConfig();
- }
-
- /**
- * Applies the configuration to be used by WebView on loading.
- *
- * If this method is not called, the configuration that is set will not be applied.
- * This method can only be called once.
- * <p>
- * Calling this method will not cause WebView to be loaded and will not block the calling
- * thread.
- *
- * @throws IllegalStateException if WebView has already been initialized
- * in the current process or if this method was called before
- */
- public void apply() {
- // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
- // WebView is going to look for it, and throw if they are not the same.
- // For this, we would need to reflect into Android Framework internals to get
- // ActivityThread.currentApplication().getClassLoader() and see if it is the same as
- // this.getClass().getClassLoader(). This would add reflection that we might not add a
- // framework API for. Once we know what framework path we will take for
- // ProcessGlobalConfig, revisit this.
- HashMap<String, Object> configMap = new HashMap<String, Object>();
- if (mApplyCalled) {
- throw new IllegalStateException("ProcessGlobalConfig#apply was "
- + "called more than once, which is an illegal operation. The configuration "
- + "settings provided by ProcessGlobalConfig take effect only once, when "
- + "WebView is first loaded into the current process. Every process should "
- + "only ever create a single instance of ProcessGlobalConfig and apply it "
- + "once, before any calls to android.webkit APIs, such as during early app "
- + "startup."
- );
- }
- mApplyCalled = true;
- if (webViewCurrentlyLoaded()) {
- throw new IllegalStateException("WebView has already been loaded in the current "
- + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
- + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
- + "calls to android.webkit APIs, such as during early app startup.");
- }
-
- final StartupApiFeature.P feature =
- WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
- if (feature.isSupportedByFramework()) {
- androidx.webkit.internal.ApiHelperForP.setDataDirectorySuffix(mDataDirectorySuffix);
- } else {
- configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX, mDataDirectorySuffix);
- }
- if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
- throw new RuntimeException("Attempting to set ProcessGlobalConfig"
- + "#sProcessGlobalConfig when it was already set");
- }
+ public ProcessGlobalConfig() {
}
/**
@@ -172,6 +102,7 @@
* @param context a Context to access application assets This value cannot be null.
* @param suffix The directory name suffix to be used for the current
* process. Must not contain a path separator and should not be empty.
+ * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
* @throws IllegalStateException if WebView has already been initialized
* in the current process or if this method was called before
* @throws IllegalArgumentException if the suffix contains a path separator or is empty.
@@ -182,11 +113,6 @@
@NonNull
public ProcessGlobalConfig setDataDirectorySuffix(@NonNull Context context,
@NonNull String suffix) {
- if (mDataDirectorySuffix != null) {
- throw new IllegalStateException(
- "ProcessGlobalConfig#setDataDirectorySuffix(String) was already "
- + "called");
- }
final StartupApiFeature.P feature =
WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
if (!feature.isSupported(context)) {
@@ -203,7 +129,62 @@
return this;
}
- private boolean webViewCurrentlyLoaded() {
+ /**
+ * Applies the configuration to be used by WebView on loading.
+ *
+ * This method can only be called once.
+ * <p>
+ * Calling this method will not cause WebView to be loaded and will not block the calling
+ * thread.
+ *
+ * @param config the config to be applied
+ * @throws IllegalStateException if WebView has already been initialized
+ * in the current process or if this method was called before
+ */
+ public static void apply(@NonNull ProcessGlobalConfig config) {
+ // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
+ // WebView is going to look for it, and throw if they are not the same.
+ // For this, we would need to reflect into Android Framework internals to get
+ // ActivityThread.currentApplication().getClassLoader() and see if it is the same as
+ // this.getClass().getClassLoader(). This would add reflection that we might not add a
+ // framework API for. Once we know what framework path we will take for
+ // ProcessGlobalConfig, revisit this.
+ synchronized (sLock) {
+ if (sApplyCalled) {
+ throw new IllegalStateException("ProcessGlobalConfig#apply was "
+ + "called more than once, which is an illegal operation. The configuration "
+ + "settings provided by ProcessGlobalConfig take effect only once, when "
+ + "WebView is first loaded into the current process. Every process should "
+ + "only ever create a single instance of ProcessGlobalConfig and apply it "
+ + "once, before any calls to android.webkit APIs, such as during early app "
+ + "startup."
+ );
+ }
+ sApplyCalled = true;
+ }
+ HashMap<String, Object> configMap = new HashMap<String, Object>();
+ if (webViewCurrentlyLoaded()) {
+ throw new IllegalStateException("WebView has already been loaded in the current "
+ + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
+ + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
+ + "calls to android.webkit APIs, such as during early app startup.");
+ }
+
+ final StartupApiFeature.P feature =
+ WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
+ if (feature.isSupportedByFramework()) {
+ ApiHelperForP.setDataDirectorySuffix(config.mDataDirectorySuffix);
+ } else {
+ configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX,
+ config.mDataDirectorySuffix);
+ }
+ if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
+ throw new RuntimeException("Attempting to set ProcessGlobalConfig"
+ + "#sProcessGlobalConfig when it was already set");
+ }
+ }
+
+ private static boolean webViewCurrentlyLoaded() {
// TODO(crbug.com/1355297): This is racy but it is the best we can do for now since we can't
// access the lock for sProviderInstance in WebView. Evaluate a framework path for
// ProcessGlobalConfig.